diff --git a/README.md b/README.md index 726751c1a..a431eb763 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ (user_id, authority_name) VALUES (@UserId, 'ROLE_ADMIN'), + (@UserId, 'ROLE_DEV'), (@UserId, 'ROLE_CURATOR'), (@UserId, 'ROLE_USER'); diff --git a/src/main/java/org/mskcc/oncokb/curation/config/application/FrontendProperties.java b/src/main/java/org/mskcc/oncokb/curation/config/application/FrontendProperties.java index a4cef2d96..0ccbc1cf2 100644 --- a/src/main/java/org/mskcc/oncokb/curation/config/application/FrontendProperties.java +++ b/src/main/java/org/mskcc/oncokb/curation/config/application/FrontendProperties.java @@ -21,4 +21,14 @@ public String getSentryDsn() { public void setSentryDsn(String sentryDsn) { this.sentryDsn = sentryDsn; } + + private Boolean stopReviewIfCoreSubmissionFails; + + public Boolean getStopReviewIfCoreSubmissionFails() { + return stopReviewIfCoreSubmissionFails; + } + + public void setStopReviewIfCoreSubmissionFails(Boolean stopReviewIfCoreSubmissionFails) { + this.stopReviewIfCoreSubmissionFails = stopReviewIfCoreSubmissionFails; + } } diff --git a/src/main/java/org/mskcc/oncokb/curation/security/AuthoritiesConstants.java b/src/main/java/org/mskcc/oncokb/curation/security/AuthoritiesConstants.java index 7a0289fe6..27dbb1834 100644 --- a/src/main/java/org/mskcc/oncokb/curation/security/AuthoritiesConstants.java +++ b/src/main/java/org/mskcc/oncokb/curation/security/AuthoritiesConstants.java @@ -7,6 +7,8 @@ public final class AuthoritiesConstants { public static final String ADMIN = "ROLE_ADMIN"; + public static final String DEV = "ROLE_DEV"; + public static final String USER = "ROLE_USER"; public static final String CURATOR = "ROLE_CURATOR"; diff --git a/src/main/java/org/mskcc/oncokb/curation/web/filter/SpaWebFilter.java b/src/main/java/org/mskcc/oncokb/curation/web/filter/SpaWebFilter.java index 3d09da66c..7defad293 100644 --- a/src/main/java/org/mskcc/oncokb/curation/web/filter/SpaWebFilter.java +++ b/src/main/java/org/mskcc/oncokb/curation/web/filter/SpaWebFilter.java @@ -23,6 +23,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse !path.startsWith("/v3/api-docs") && !path.startsWith("/login") && !path.startsWith("/oauth2") && + !path.startsWith("/legacy-api") && !path.startsWith("/websocket") && !path.contains(".") && path.matches("/(.*)") diff --git a/src/main/java/org/mskcc/oncokb/curation/web/rest/ApiProxy.java b/src/main/java/org/mskcc/oncokb/curation/web/rest/ApiProxy.java index 9b19f9b85..27105face 100644 --- a/src/main/java/org/mskcc/oncokb/curation/web/rest/ApiProxy.java +++ b/src/main/java/org/mskcc/oncokb/curation/web/rest/ApiProxy.java @@ -6,14 +6,14 @@ import java.nio.charset.StandardCharsets; import org.mskcc.oncokb.curation.service.ApiProxyService; import org.mskcc.oncokb.curation.web.rest.errors.BadRequestAlertException; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; +import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; @@ -29,8 +29,34 @@ public ApiProxy(ApiProxyService apiProxyService) { this.apiProxyService = apiProxyService; } - @RequestMapping("/**") - public ResponseEntity proxy(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request) + @RequestMapping(value = "/**", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public ResponseEntity formDataProxy( + @RequestParam MultiValueMap formParams, + HttpMethod method, + HttpServletRequest request + ) throws URISyntaxException { + URI uri = apiProxyService.prepareURI(request); + + HttpHeaders httpHeaders = apiProxyService.prepareHttpHeaders(request.getContentType()); + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getMessageConverters().add(0, new FormHttpMessageConverter()); + + HttpEntity> requestEntity = new HttpEntity<>(formParams, httpHeaders); + try { + return restTemplate.exchange(uri, method, requestEntity, String.class); + } catch (HttpClientErrorException httpClientErrorException) { + if ( + httpClientErrorException.getStatusCode() != null && httpClientErrorException.getStatusCode().equals(HttpStatus.BAD_REQUEST) + ) { + throw new BadRequestAlertException(httpClientErrorException.getMessage(), "", ""); + } else { + throw new ResponseStatusException(httpClientErrorException.getStatusCode(), httpClientErrorException.getMessage()); + } + } + } + + @RequestMapping(value = "/**", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity jsonProxy(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request) throws URISyntaxException { URI uri = apiProxyService.prepareURI(request); diff --git a/src/main/resources/config/liquibase/data/authority.csv b/src/main/resources/config/liquibase/data/authority.csv index 01446a74e..bcff3109f 100644 --- a/src/main/resources/config/liquibase/data/authority.csv +++ b/src/main/resources/config/liquibase/data/authority.csv @@ -1,4 +1,5 @@ name ROLE_ADMIN +ROLE_DEV ROLE_USER ROLE_CURATOR diff --git a/src/main/webapp/app/appConfig.ts b/src/main/webapp/app/appConfig.ts index 86533b4fc..522664074 100644 --- a/src/main/webapp/app/appConfig.ts +++ b/src/main/webapp/app/appConfig.ts @@ -1,6 +1,7 @@ type FrontendProperties = { firebase?: FirebaseProperties; sentryDsn?: string; + stopReviewIfCoreSubmissionFails?: boolean; }; export type FirebaseProperties = { diff --git a/src/main/webapp/app/components/diff-viewer/DiffViewer.tsx b/src/main/webapp/app/components/diff-viewer/DiffViewer.tsx index ca838d153..6abb5027c 100644 --- a/src/main/webapp/app/components/diff-viewer/DiffViewer.tsx +++ b/src/main/webapp/app/components/diff-viewer/DiffViewer.tsx @@ -19,7 +19,7 @@ type DiffViewerProps = { className?: string; }; -const getMergedDiff = (newContent='', oldContent='') => { +const getMergedDiff = (newContent = '', oldContent = '') => { const dmp = new DiffMatchPatch(); const diff = dmp.diff_main(oldContent, newContent); dmp.diff_cleanupSemantic(diff); diff --git a/src/main/webapp/app/components/tabs/CurationToolsTab.tsx b/src/main/webapp/app/components/tabs/CurationToolsTab.tsx index eeef4e0b3..db0ce5c5b 100644 --- a/src/main/webapp/app/components/tabs/CurationToolsTab.tsx +++ b/src/main/webapp/app/components/tabs/CurationToolsTab.tsx @@ -1,18 +1,21 @@ import { componentInject } from 'app/shared/util/typed-inject'; -import { IRootStore } from 'app/stores'; +import { IRootStore, hasAnyAuthority } from 'app/stores'; import React, { useEffect, useRef, useState } from 'react'; import { Button, Col, Row } from 'reactstrap'; import { FaRegCheckCircle } from 'react-icons/fa'; -import { FaRegCircleXmark } from 'react-icons/fa6'; -import { notifyError } from 'app/oncokb-commons/components/util/NotificationUtils'; +import { FaRegCircleXmark, FaArrowRotateLeft } from 'react-icons/fa6'; +import { notifyError, notifySuccess } from 'app/oncokb-commons/components/util/NotificationUtils'; import { IGene } from 'app/shared/model/gene.model'; import _ from 'lodash'; import { IFlag } from 'app/shared/model/flag.model'; -import { CURRENT_REVIEWER } from 'app/config/constants/constants'; +import { AUTHORITIES, CURRENT_REVIEWER } from 'app/config/constants/constants'; import { GeneType } from 'app/shared/model/firebase/firebase.model'; import { onValue, ref } from 'firebase/database'; import { FB_COLLECTION } from 'app/config/constants/firebase'; +import SaveGeneButton from 'app/shared/button/SaveGeneButton'; import { Unsubscribe } from 'firebase/database'; +import { geneIsReleased } from 'app/shared/util/entity-utils/gene-entity-utils'; +import Tooltip from 'rc-tooltip'; export type ReleaseGeneTestData = { passed: boolean; @@ -22,6 +25,8 @@ export type ReleaseGeneTestData = { export interface ICurationToolsTabProps extends StoreProps { genePath: string; + isGermline: boolean; + hugoSymbol: string; } export function CurationToolsTab({ @@ -33,6 +38,11 @@ export function CurationToolsTab({ searchGenes, updateGene, searchFlags, + isGermline, + hugoSymbol, + isDev, + firebaseGeneService, + geneLegacyApi, }: ICurationToolsTabProps) { const [geneName, setGeneName] = useState(); const [geneSummary, setGeneSummary] = useState(); @@ -114,7 +124,7 @@ export function CurationToolsTab({ useEffect(() => { const geneData = geneEntities?.find(entity => entity.hugoSymbol === geneName); - setIsReleased(geneData?.flags?.some(flag => isReleasedFlag(flag)) || false); + setIsReleased(geneData === undefined ? false : geneIsReleased(geneData)); geneToUpdate.current = geneData; }, [geneEntities, geneName]); @@ -146,6 +156,28 @@ export function CurationToolsTab({ } await updateGene?.(newGene); await searchGenes?.({ query: geneName, exact: true }); // repopulate gene store entities + await firebaseGeneService?.saveGene(isGermline, hugoSymbol); + } catch (error) { + notifyError(error); + } + } + + async function handleUndoReleaseGeneConfirmClick() { + const newGene = _.cloneDeep(geneToUpdate.current); + + try { + if (!newGene) { + throw new Error('Error retrieving gene'); + } + + if (newGene.flags) { + newGene.flags = newGene.flags.filter(flag => !isReleasedFlag(flag)); + } else { + newGene.flags = []; + } + await updateGene?.(newGene); + await searchGenes?.({ query: geneName, exact: true }); // repopulate gene store entities + geneLegacyApi!.removeGene(newGene); } catch (error) { notifyError(error); } @@ -159,12 +191,26 @@ export function CurationToolsTab({ if (isReleased) { return ( - - - - Gene is released - - + <> + + + + Gene is released + + + + + + {!isGermline && isDev && ( + + + + + + )} + ); } @@ -192,16 +238,26 @@ export function CurationToolsTab({ } return ( - +
+ +
); } return getContent(); } -const mapStoreToProps = ({ firebaseAppStore, firebaseMetaStore, geneStore, flagStore }: IRootStore) => ({ +const mapStoreToProps = ({ + firebaseAppStore, + firebaseMetaStore, + geneStore, + flagStore, + authStore, + firebaseGeneService, + geneLegacyApi, +}: IRootStore) => ({ firebaseDb: firebaseAppStore.firebaseDb, metaList: firebaseMetaStore.data, addMetaListListener: firebaseMetaStore.addListener, @@ -209,6 +265,9 @@ const mapStoreToProps = ({ firebaseAppStore, firebaseMetaStore, geneStore, flagS searchGenes: geneStore.searchEntities, updateGene: geneStore.updateEntity, searchFlags: flagStore.searchEntities, + isDev: hasAnyAuthority(authStore.account.authorities, [AUTHORITIES.DEV]), + firebaseGeneService, + geneLegacyApi, }); type StoreProps = Partial>; diff --git a/src/main/webapp/app/components/tabs/GeneListPageToolsTab.tsx b/src/main/webapp/app/components/tabs/GeneListPageToolsTab.tsx index 943e69ed5..f9c6573ec 100644 --- a/src/main/webapp/app/components/tabs/GeneListPageToolsTab.tsx +++ b/src/main/webapp/app/components/tabs/GeneListPageToolsTab.tsx @@ -1,19 +1,21 @@ -import { CURATE_NEW_GENE_TEXT, DEFAULT_ICON_SIZE, PAGE_ROUTE } from 'app/config/constants/constants'; +import { AUTHORITIES, CURATE_NEW_GENE_TEXT, DEFAULT_ICON_SIZE, PAGE_ROUTE } from 'app/config/constants/constants'; import GeneSelect from 'app/shared/select/GeneSelect'; import { componentInject } from 'app/shared/util/typed-inject'; -import { IRootStore } from 'app/stores'; +import { IRootStore, hasAnyAuthority } from 'app/stores'; import { observer } from 'mobx-react'; import React, { useRef, useState } from 'react'; import { FaExclamationCircle } from 'react-icons/fa'; -import { Button } from 'reactstrap'; +import { Button, Col, Row } from 'reactstrap'; import './curation-tools-tab.scss'; import { MetaCollection } from 'app/shared/model/firebase/firebase.model'; +import SaveGeneButton from 'app/shared/button/SaveGeneButton'; export interface IGeneListPageToolsTab extends StoreProps { metaData: MetaCollection | null; + isGermline: boolean; } -function GeneListPageToolsTab({ metaData, createGene }: IGeneListPageToolsTab) { +function GeneListPageToolsTab({ metaData, isDev, createGene, isGermline }: IGeneListPageToolsTab) { const selectedGene = useRef(); const [createButtonDisabled, setCreateButtonDisabled] = useState(true); const [showGeneExistsWarning, setShowGeneExistsWarning] = useState(false); @@ -41,28 +43,42 @@ function GeneListPageToolsTab({ metaData, createGene }: IGeneListPageToolsTab) { } return ( -
-
{CURATE_NEW_GENE_TEXT}
-
- -
-
- {showGeneExistsWarning && ( -
- - Gene already exists + <> + + +
+
{CURATE_NEW_GENE_TEXT}
+
+ +
+
+ {showGeneExistsWarning && ( +
+ + Gene already exists +
+ )} + +
- )} - -
-
+ + + {!isGermline && isDev && ( + +
+ +
+
+ )} + ); } -const mapStoreToProps = ({ firebaseGeneService }: IRootStore) => ({ +const mapStoreToProps = ({ firebaseGeneService, authStore }: IRootStore) => ({ createGene: firebaseGeneService.createGene, + isDev: hasAnyAuthority(authStore.account.authorities, [AUTHORITIES.DEV]), }); type StoreProps = Partial>; diff --git a/src/main/webapp/app/config/constants/constants.ts b/src/main/webapp/app/config/constants/constants.ts index 4efaa83d0..e4fde0e26 100644 --- a/src/main/webapp/app/config/constants/constants.ts +++ b/src/main/webapp/app/config/constants/constants.ts @@ -4,6 +4,7 @@ import { ToastOptions } from 'react-toastify'; export const AUTHORITIES = { ADMIN: 'ROLE_ADMIN', + DEV: 'ROLE_DEV', USER: 'ROLE_USER', CURATOR: 'ROLE_CURATOR', }; @@ -336,6 +337,7 @@ export enum SearchOptionType { export enum USER_AUTHORITY { ROLE_USER = 'ROLE_USER', ROLE_ADMIN = 'ROLE_ADMIN', + ROLE_DEV = 'ROLE_DEV', ROLE_CURATOR = 'ROLE_CURATOR', } diff --git a/src/main/webapp/app/entities/drug/drug-update.tsx b/src/main/webapp/app/entities/drug/drug-update.tsx index 264f3ea88..da8786ede 100644 --- a/src/main/webapp/app/entities/drug/drug-update.tsx +++ b/src/main/webapp/app/entities/drug/drug-update.tsx @@ -139,7 +139,7 @@ export const DrugUpdate = (props: IDrugUpdateProps) => { {flags ? flags.map(otherEntity => ( )) : null} diff --git a/src/main/webapp/app/pages/curation/CurationPage.tsx b/src/main/webapp/app/pages/curation/CurationPage.tsx index 465d28869..61774f0ee 100644 --- a/src/main/webapp/app/pages/curation/CurationPage.tsx +++ b/src/main/webapp/app/pages/curation/CurationPage.tsx @@ -250,7 +250,7 @@ export const CurationPage = (props: ICurationPageProps) => { tabs={[ { title: 'Tools', - content: , + content: , }, { title: 'History', diff --git a/src/main/webapp/app/pages/curation/GeneListPage.tsx b/src/main/webapp/app/pages/curation/GeneListPage.tsx index 1019e3a7b..72ccda737 100644 --- a/src/main/webapp/app/pages/curation/GeneListPage.tsx +++ b/src/main/webapp/app/pages/curation/GeneListPage.tsx @@ -95,7 +95,7 @@ const GeneListPage = (props: IGeneListPage) => { const tabs = [ { title: 'Tools', - content: , + content: , }, ] as Tab[]; if (!isGermline) { diff --git a/src/main/webapp/app/pages/curation/collapsible/ReviewCollapsible.tsx b/src/main/webapp/app/pages/curation/collapsible/ReviewCollapsible.tsx index 5bbd71567..e1cef0872 100644 --- a/src/main/webapp/app/pages/curation/collapsible/ReviewCollapsible.tsx +++ b/src/main/webapp/app/pages/curation/collapsible/ReviewCollapsible.tsx @@ -21,6 +21,8 @@ import { CollapsibleColorProps, CollapsibleDisplayProps } from './BaseCollapsibl import { getReviewInfo, getTxName } from 'app/shared/util/firebase/firebase-utils'; import { notifyError } from 'app/oncokb-commons/components/util/NotificationUtils'; import DiffViewer, { FirebaseContent } from 'app/components/diff-viewer/DiffViewer'; +import { FirebaseGeneReviewService } from 'app/service/firebase/firebase-gene-review-service'; +import { DrugCollection, Gene } from 'app/shared/model/firebase/firebase.model'; import { IDrug } from 'app/shared/model/drug.model'; import { ReviewCollapsibleTitle } from './ReviewCollapsibleTitle'; import { Review } from 'app/shared/model/firebase/firebase.model'; @@ -66,13 +68,16 @@ const ReviewCollapsibleBootstrapClass = { const TREATMENT_NAME_REGEX = /treatments\/\d+\/name$/; export interface IReviewCollapsibleProps { + drugListRef: DrugCollection; + entrezGeneId: number; + gene: Gene; hugoSymbol: string; baseReviewLevel: BaseReviewLevel; isGermline: boolean; firebase: FirebaseContent; parentDelete?: (reviewlLevelId: string, action: ActionType, isPending?: boolean) => void; rootDelete?: (isPending?: boolean) => void; - handleAccept?: (hugoSymbol: string, reviewLevels: ReviewLevel[], isGermline: boolean, isAcceptAll?: boolean) => Promise; + handleAccept?: FirebaseGeneReviewService['acceptChanges']; handleReject?: (hugoSymbol: string, reviewLevels: ReviewLevel[], isGermline: boolean) => Promise; handleCreateAction?: (hugoSymbol: string, reviewLevel: ReviewLevel, isGermline: boolean, action: ActionType) => Promise; disableActions?: boolean; @@ -93,6 +98,9 @@ export const ReviewCollapsible = ({ drugList, disableActions = false, isRoot = false, + drugListRef, + entrezGeneId, + gene, }: IReviewCollapsibleProps) => { const [rootReview, setRootReview] = useState(baseReviewLevel); const [reviewChildren, setReviewChildren] = useState([]); @@ -201,7 +209,7 @@ export const ReviewCollapsible = ({ // After marking collapsible as pending, it will be removed from the view. Now we need to save to firebase try { if (action === ActionType.ACCEPT) { - await handleAccept?.(hugoSymbol, getReviewLevelsForActions(), isGermline); + await handleAccept?.({ hugoSymbol, reviewLevels: getReviewLevelsForActions(), isGermline, drugListRef, entrezGeneId, gene }); } else if (action === ActionType.REJECT) { await handleReject?.(hugoSymbol, getReviewLevelsForActions(), isGermline); } @@ -390,6 +398,9 @@ export const ReviewCollapsible = ({ db: firebase?.db, }} drugList={drugList} + gene={gene} + entrezGeneId={entrezGeneId} + drugListRef={drugListRef} /> )); } else { diff --git a/src/main/webapp/app/pages/curation/header/MutationsSectionHeader.tsx b/src/main/webapp/app/pages/curation/header/MutationsSectionHeader.tsx index 15e1af95d..a4bab1f1a 100644 --- a/src/main/webapp/app/pages/curation/header/MutationsSectionHeader.tsx +++ b/src/main/webapp/app/pages/curation/header/MutationsSectionHeader.tsx @@ -142,7 +142,7 @@ function MutationsSectionHeader({ const selectedMutationEffects = mutationEffectFilter.filter(filter => filter.selected); const matchesMutationEffect = selectedMutationEffects.length === 0 || - selectedMutationEffects.some(mutationEffect => mutationEffect.label === mutation.mutation_effect.effect); + selectedMutationEffects.some(mutationEffect => mutationEffect.label === (mutation.mutation_effect.effect as string)); const selectedHotspot = hotspotFilter.filter(filter => filter.selected).map(option => option.label); diff --git a/src/main/webapp/app/pages/curation/review/ReviewPage.tsx b/src/main/webapp/app/pages/curation/review/ReviewPage.tsx index ec611078b..b1318201a 100644 --- a/src/main/webapp/app/pages/curation/review/ReviewPage.tsx +++ b/src/main/webapp/app/pages/curation/review/ReviewPage.tsx @@ -6,14 +6,14 @@ import { getCompactReviewInfo, getGenePathFromValuePath, } from 'app/shared/util/firebase/firebase-review-utils'; -import { getFirebaseGenePath, getFirebaseMetaGenePath } from 'app/shared/util/firebase/firebase-utils'; +import { getFirebaseGenePath, getFirebaseMetaGenePath, getFirebasePath } from 'app/shared/util/firebase/firebase-utils'; import { componentInject } from 'app/shared/util/typed-inject'; -import { getSectionClassName } from 'app/shared/util/utils'; +import { getSectionClassName, useDrugListRef } from 'app/shared/util/utils'; import { IRootStore } from 'app/stores'; import { get, ref } from 'firebase/database'; import { observer } from 'mobx-react'; import React, { useEffect, useState } from 'react'; -import { Alert, Col, FormGroup, Input, Label, Row } from 'reactstrap'; +import { Alert, Col, Row } from 'reactstrap'; import { RouteComponentProps } from 'react-router-dom'; import { useMatchGeneEntity } from 'app/hooks/useMatchGeneEntity'; import { GERMLINE_PATH, GET_ALL_DRUGS_PAGE_SIZE } from 'app/config/constants/constants'; @@ -23,6 +23,8 @@ import _ from 'lodash'; import { ReviewCollapsible } from '../collapsible/ReviewCollapsible'; import { notifyError } from 'app/oncokb-commons/components/util/NotificationUtils'; import { AsyncSaveButton } from 'app/shared/button/AsyncSaveButton'; +import { Gene, MetaReview } from 'app/shared/model/firebase/firebase.model'; +import { SentryError } from 'app/config/sentry-error'; interface IReviewPageProps extends StoreProps, RouteComponentProps<{ hugoSymbol: string }> {} @@ -36,8 +38,8 @@ const ReviewPage: React.FunctionComponent = (props: IReviewPag const firebaseGenePath = getFirebaseGenePath(isGermline, hugoSymbol); const firebaseMetaReviewPath = `${getFirebaseMetaGenePath(isGermline, hugoSymbol)}/review`; - const [geneData, setGeneData] = useState(null); - const [metaReview, setMetaReview] = useState(null); + const [geneData, setGeneData] = useState(null); + const [metaReview, setMetaReview] = useState(null); const [isReviewFinished, setIsReviewFinished] = useState(false); @@ -67,6 +69,8 @@ const ReviewPage: React.FunctionComponent = (props: IReviewPag } }, [geneEntity, props.firebaseDb, props.firebaseInitSuccess]); + const drugListRef = useDrugListRef(props.drugList); + useEffect(() => { props.getDrugs?.({ page: 0, size: GET_ALL_DRUGS_PAGE_SIZE, sort: ['id,asc'] }); }, []); @@ -102,13 +106,28 @@ const ReviewPage: React.FunctionComponent = (props: IReviewPag }, [geneData, reviewUuids, props.drugList]); const acceptAllChangesFromEditors = async (editors: string[]) => { + if (hugoSymbol === undefined) { + notifyError(new SentryError('Cannot accept all changes because hugo symbol is unknown.', { hugoSymbol, geneData })); + return; + } else if (geneData === null) { + notifyError(new SentryError('Cannot accept all changes because gene data is unknown.', { hugoSymbol, geneData })); + return; + } let reviewLevels = [] as ReviewLevel[]; for (const editor of editors) { reviewLevels = reviewLevels.concat(editorReviewMap.getReviewsByEditor(editor)); } try { setIsAcceptingAll(true); - await props.acceptReviewChangeHandler?.(hugoSymbol ?? '', reviewLevels, isGermline, true); + await props.acceptReviewChangeHandler?.({ + hugoSymbol, + reviewLevels, + isGermline, + isAcceptAll: true, + gene: geneData, + entrezGeneId: geneEntity?.entrezGeneId as number, + drugListRef, + }); await fetchFirebaseData(); } catch (error) { notifyError(error); @@ -120,7 +139,12 @@ const ReviewPage: React.FunctionComponent = (props: IReviewPag const allEditors = editorReviewMap.getEditorList(); - return props.firebaseInitSuccess && !props.loadingGenes && props.drugList !== undefined && props.drugList.length > 0 && !!geneEntity ? ( + return props.firebaseInitSuccess && + !props.loadingGenes && + props.drugList !== undefined && + props.drugList.length > 0 && + !!geneEntity && + !isAcceptingAll ? (
= (props: IReviewPag { + handleAccept={async args => { setIsAccepting(true); try { - const returnVal = await props.acceptReviewChangeHandler?.(hugoArg, reviewLevelsArg, isGermlineArg, isAcceptAllArg); + const returnVal = await props.acceptReviewChangeHandler?.(args); if (returnVal?.shouldRefresh) { await fetchFirebaseData(); } diff --git a/src/main/webapp/app/service/firebase/firebase-gene-review-service.spec.ts b/src/main/webapp/app/service/firebase/firebase-gene-review-service.spec.ts index 81876d0ea..8ba9bedad 100644 --- a/src/main/webapp/app/service/firebase/firebase-gene-review-service.spec.ts +++ b/src/main/webapp/app/service/firebase/firebase-gene-review-service.spec.ts @@ -10,9 +10,12 @@ import { SentryError } from 'app/config/sentry-error'; import { getTumorNameUuid, ReviewLevel, TumorReviewLevel } from 'app/shared/util/firebase/firebase-review-utils'; import { ReviewAction } from 'app/config/constants/firebase'; import _ from 'lodash'; -import { generateUuid } from 'app/shared/util/utils'; import { FIREBASE_LIST_PATH_TYPE } from 'app/shared/util/firebase/firebase-path-utils'; import { ActionType } from 'app/pages/curation/collapsible/ReviewCollapsible'; +import { EvidenceApi } from 'app/shared/api/manual/evidence-api'; +import { createMockGene, createMockMutation, createMockTumor } from 'app/shared/util/core-submission-shared/core-submission.mocks'; +import { GeneTypeApi } from 'app/shared/api/manual/gene-type-api'; +import { generateUuid } from 'app/shared/util/utils'; describe('Firebase Gene Review Service', () => { const DEFAULT_USERNAME = 'Test User'; @@ -25,6 +28,8 @@ describe('Firebase Gene Review Service', () => { const mockMetaService = mock(); const mockHistoryService = mock(); const mockVusService = mock(); + const mockEvidenceClient = mock(); + const mockGeneTypeClient = mock(); let firebaseGeneReviewService: FirebaseGeneReviewService; beforeEach(() => { @@ -39,6 +44,8 @@ describe('Firebase Gene Review Service', () => { mockMetaService, mockHistoryService, mockVusService, + mockEvidenceClient, + mockGeneTypeClient, ); jest.useFakeTimers().setSystemTime(DEFAULT_DATE); @@ -54,7 +61,7 @@ describe('Firebase Gene Review Service', () => { }); mockVusService.getVusUpdateObject.mockImplementation((path, variants) => { - const originalVusService = new FirebaseVusService(mockFirebaseRepository, mockAuthStore); + const originalVusService = new FirebaseVusService(mockFirebaseRepository, mockEvidenceClient, mockAuthStore); return originalVusService.getVusUpdateObject(path, variants); }); @@ -169,7 +176,16 @@ describe('Firebase Gene Review Service', () => { }, historyInfo: {}, }); - await firebaseGeneReviewService.acceptChanges(hugoSymbol, [reviewLevel], false); + + await firebaseGeneReviewService.acceptChanges({ + hugoSymbol, + reviewLevels: [reviewLevel], + isGermline: false, + gene, + drugListRef: {}, + entrezGeneId: 0, + }); + // We expect the lastReviewed to be cleared when accepting changes expect(mockFirebaseRepository.update.mock.calls[0][0]).toEqual('/'); expect(mockFirebaseRepository.update.mock.calls[0][1]).toMatchObject({ @@ -215,7 +231,19 @@ describe('Firebase Gene Review Service', () => { }, historyInfo: {}, }); - await firebaseGeneReviewService.acceptChanges(hugoSymbol, [reviewLevel], false); + + await firebaseGeneReviewService.acceptChanges({ + hugoSymbol, + reviewLevels: [reviewLevel], + isGermline: false, + gene: createMockGene({ + name: hugoSymbol, + mutations: [createMockMutation()], + }), + drugListRef: {}, + entrezGeneId: 0, + }); + expect(mockFirebaseRepository.deleteFromArray).toHaveBeenCalledWith('Genes/BRAF/mutations', [0]); expect(mockFirebaseRepository.update.mock.calls[0][0]).toEqual('/'); expect(mockFirebaseRepository.update.mock.calls[0][1]).toMatchObject({ @@ -257,7 +285,19 @@ describe('Firebase Gene Review Service', () => { }, historyInfo: {}, }); - await firebaseGeneReviewService.acceptChanges(hugoSymbol, [reviewLevel], false); + + await firebaseGeneReviewService.acceptChanges({ + hugoSymbol, + reviewLevels: [reviewLevel], + isGermline: false, + gene: createMockGene({ + name: hugoSymbol, + mutations: [createMockMutation()], + }), + drugListRef: {}, + entrezGeneId: 0, + }); + expect(mockFirebaseRepository.deleteFromArray).toHaveBeenCalledWith('Genes/BRAF/mutations', [0]); // We expect both alterations (V600E and V600K) to be added to VUS list expect(mockFirebaseRepository.update.mock.calls[0][0]).toEqual('/'); @@ -391,12 +431,32 @@ describe('Firebase Gene Review Service', () => { historyInfo: {}, }); - await firebaseGeneReviewService.acceptChanges( + const mutations: Mutation[] = []; + for (let i = 0; i < 23; i++) { + const tumors: Tumor[] = []; + for (let j = 0; j < 2; j++) { + tumors.push(createMockTumor({})); + } + mutations.push( + createMockMutation({ + tumors, + }), + ); + } + + const gene = createMockGene({ + mutations, + }); + + await firebaseGeneReviewService.acceptChanges({ hugoSymbol, - [mutationReviewLevel, tumorReviewLevel, tumorSummaryReviewLevel], - false, - true, - ); + reviewLevels: [mutationReviewLevel, tumorReviewLevel, tumorSummaryReviewLevel], + isGermline: false, + isAcceptAll: true, + gene, + drugListRef: {}, + entrezGeneId: 0, + }); // Multi-location updates should happen before deleting from array to ensure that indices are not stale expect(mockFirebaseRepository.update.mock.invocationCallOrder[0]).toBeLessThan( @@ -544,6 +604,65 @@ describe('Firebase Gene Review Service', () => { }); }); + it('should reject initial excluded cancer type', async () => { + const mutation = new Mutation('V600E'); + const tumor = new Tumor(); + tumor.cancerTypes = [{ code: '', subtype: '', mainType: 'Melanoma' }]; + tumor.cancerTypes_review = new Review('User'); + tumor.excludedCancerTypes = [{ code: 'OCM', subtype: 'Ocular Melanoma', mainType: 'Melanoma' }]; + tumor.excludedCancerTypes_review = new Review('User', undefined, false, false, true); + tumor.excludedCancerTypes_uuid = generateUuid(); + mutation.tumors.push(tumor); + + const reviewLevel = new TumorReviewLevel({ + titleParts: ['Oncogenic Mutations', 'Breast Cancer {excluding Metaplastic Breast Cancer}', 'Name'], + valuePath: 'mutations/0/tumors/0/cancerTypes', + historyLocation: 'Oncogenic Mutations, Breast Cancer {excluding Metaplastic Breast Cancer}', + children: [], + historyInfo: {}, + currentVal: 'Breast Cancer {excluding Metaplastic Breast Cancer}', + reviewInfo: { + reviewPath: 'mutations/0/tumors/0/cancerTypes_review', + review: tumor.cancerTypes_review, + lastReviewedString: 'Breast Cancer', + uuid: getTumorNameUuid(tumor.cancerTypes_uuid, tumor.excludedCancerTypes_uuid), + reviewAction: 3, + }, + historyData: { + oldState: 'Breast Cancer', + newState: 'Breast Cancer {excluding Metaplastic Breast Cancer}', + }, + excludedCancerTypesReviewInfo: { + reviewPath: 'mutations/0/tumors/0/excludedCancerTypes_review', + review: tumor.excludedCancerTypes_review, + lastReviewedString: undefined, + uuid: tumor.excludedCancerTypes_uuid, + }, + currentExcludedCancerTypes: [ + { + code: 'MBC', + mainType: 'Breast Cancer', + subtype: 'Metaplastic Breast Cancer', + }, + ], + }); + + await firebaseGeneReviewService.rejectChanges('BRAF', [reviewLevel], false); + expect(mockFirebaseRepository.update.mock.calls[0][0]).toEqual('/'); + expect(mockFirebaseRepository.update.mock.calls[0][1]).toMatchObject({ + 'Genes/BRAF/mutations/0/tumors/0/cancerTypes_review': { + updateTime: DEFAULT_DATE.getTime(), + updatedBy: mockAuthStore.fullName, + }, + 'Genes/BRAF/mutations/0/tumors/0/excludedCancerTypes': null, + 'Meta/BRAF/lastModifiedAt': DEFAULT_DATETIME_STRING, + 'Meta/BRAF/lastModifiedBy': mockAuthStore.fullName, + [`Meta/BRAF/review/${getTumorNameUuid(tumor.cancerTypes_uuid, tumor.excludedCancerTypes_uuid)}`]: null, + [`Meta/BRAF/review/${tumor.cancerTypes_uuid}`]: null, + [`Meta/BRAF/review/${tumor.excludedCancerTypes_uuid}`]: null, + }); + }); + it('should reject initial excluded RCT', async () => { const mutation = new Mutation('V600E'); const tumor = new Tumor(); diff --git a/src/main/webapp/app/service/firebase/firebase-gene-review-service.ts b/src/main/webapp/app/service/firebase/firebase-gene-review-service.ts index b63bf5dbd..fafaf6680 100644 --- a/src/main/webapp/app/service/firebase/firebase-gene-review-service.ts +++ b/src/main/webapp/app/service/firebase/firebase-gene-review-service.ts @@ -4,7 +4,7 @@ import { FirebaseMetaService } from 'app/service/firebase/firebase-meta-service' import { AuthStore } from 'app/stores'; import { FirebaseRepository } from 'app/stores/firebase/firebase-repository'; import _ from 'lodash'; -import { Review } from '../../shared/model/firebase/firebase.model'; +import { DrugCollection, Gene, Review } from '../../shared/model/firebase/firebase.model'; import { buildHistoryFromReviews } from '../../shared/util/firebase/firebase-history-utils'; import { extractArrayPath, @@ -29,6 +29,16 @@ import { SentryError } from 'app/config/sentry-error'; import { ActionType } from 'app/pages/curation/collapsible/ReviewCollapsible'; export type ItemsToDeleteMap = { [key in FIREBASE_LIST_PATH_TYPE]: { [path in string]: number[] } }; +import { + getEvidence, + pathToDeleteEvidenceArgs, + pathToGetEvidenceArgs, +} from 'app/shared/util/core-evidence-submission/core-evidence-submission'; +import { EvidenceApi } from 'app/shared/api/manual/evidence-api'; +import { createGeneTypePayload, isGeneTypeChange } from 'app/shared/util/core-gene-type-submission/core-gene-type-submission'; +import { GeneTypeApi } from 'app/shared/api/manual/gene-type-api'; +import { flattenReviewPaths, useLastReviewedOnly } from 'app/shared/util/core-submission-shared/core-submission-utils'; +import { AppConfig } from 'app/appConfig'; export class FirebaseGeneReviewService { firebaseRepository: FirebaseRepository; @@ -36,6 +46,8 @@ export class FirebaseGeneReviewService { firebaseMetaService: FirebaseMetaService; firebaseHistoryService: FirebaseHistoryService; firebaseVusService: FirebaseVusService; + evidenceClient: EvidenceApi; + geneTypeClient: GeneTypeApi; constructor( firebaseRepository: FirebaseRepository, @@ -43,12 +55,16 @@ export class FirebaseGeneReviewService { firebaseMetaService: FirebaseMetaService, firebaseHistoryService: FirebaseHistoryService, firebaseVusService: FirebaseVusService, + evidenceClient: EvidenceApi, + geneTypeClient: GeneTypeApi, ) { this.firebaseRepository = firebaseRepository; this.authStore = authStore; this.firebaseMetaService = firebaseMetaService; this.firebaseHistoryService = firebaseHistoryService; this.firebaseVusService = firebaseVusService; + this.evidenceClient = evidenceClient; + this.geneTypeClient = geneTypeClient; } getGeneUpdateObject = (updateValue: any, updatedReview: Review, firebasePath: string, uuid: string | undefined) => { @@ -98,7 +114,29 @@ export class FirebaseGeneReviewService { } }; - acceptChanges = async (hugoSymbol: string, reviewLevels: ReviewLevel[], isGermline: boolean, isAcceptAll = false) => { + acceptChanges = async ({ + gene, + hugoSymbol, + reviewLevels, + isGermline, + isAcceptAll = false, + drugListRef, + entrezGeneId, + }: { + gene: Gene; + hugoSymbol: string; + reviewLevels: ReviewLevel[]; + isGermline: boolean; + isAcceptAll?: boolean; + drugListRef: DrugCollection; + entrezGeneId: number; + }): Promise< + | { + shouldRefresh: boolean; + } + | undefined + | void + > => { const geneFirebasePath = getFirebaseGenePath(isGermline, hugoSymbol); const vusFirebasePath = getFirebaseVusPath(isGermline, hugoSymbol); @@ -108,6 +146,92 @@ export class FirebaseGeneReviewService { [FIREBASE_LIST_PATH_TYPE.TREATMENT_LIST]: {}, }; + let evidences: ReturnType = {}; + let geneTypePayload: ReturnType | undefined = undefined; + let hasEvidences = false; + try { + const flattenedReviewLevels = reviewLevels.flatMap(flattenReviewPaths); + // Generate a new version of the gene object (`approvedGene`) for the getEvidence payload. + // This ensures that if multiple valuePaths modify the same part of the payload, + // the changes are applied consistently, preventing any section from being overwritten unintentionally. + const approvedGene = useLastReviewedOnly(gene, ...flattenedReviewLevels.map(x => x.valuePath)) as Gene; + for (const reviewLevel of flattenedReviewLevels) { + if (!isCreateReview(reviewLevel)) { + if (reviewLevel.reviewInfo.review.removed) { + const deleteEvidencesPayload = pathToDeleteEvidenceArgs({ valuePath: reviewLevel.valuePath, gene }); + if (deleteEvidencesPayload !== undefined) { + this.evidenceClient.deleteEvidences(deleteEvidencesPayload); + } + } else if (isGeneTypeChange(reviewLevel.valuePath)) { + geneTypePayload = createGeneTypePayload(approvedGene); + } else { + const args = pathToGetEvidenceArgs({ + gene: approvedGene, + valuePath: reviewLevel.valuePath, + updateTime: new Date().getTime(), + drugListRef, + entrezGeneId, + }); + if (args !== undefined) { + evidences = { + ...evidences, + ...getEvidence(args), + }; + hasEvidences = true; + } + } + } + } + } catch (error) { + const sentryError = new SentryError('Failed to create evidences when accepting changes in review mode', { + hugoSymbol, + reviewLevels, + isGermline, + error, + }); + if (AppConfig.serverConfig.frontend?.stopReviewIfCoreSubmissionFails) { + throw sentryError; + } else { + console.error(sentryError); + } + } + + try { + if (geneTypePayload) { + await this.geneTypeClient.submitGeneTypeToCore(geneTypePayload); + } + } catch (error) { + const sentryError = new SentryError('Failed to submit evidences to core when accepting changes in review mode', { + hugoSymbol, + reviewLevels, + isGermline, + error, + }); + if (AppConfig.serverConfig.frontend?.stopReviewIfCoreSubmissionFails) { + throw sentryError; + } else { + console.error(sentryError); + } + } + + try { + if (hasEvidences) { + await this.evidenceClient.submitEvidences(evidences); + } + } catch (error) { + const sentryError = new SentryError('Failed to submit evidences to core when accepting changes in review mode', { + hugoSymbol, + reviewLevels, + isGermline, + error, + }); + if (AppConfig.serverConfig.frontend?.stopReviewIfCoreSubmissionFails) { + throw sentryError; + } else { + console.error(sentryError); + } + } + let updateObject = {}; const reviewHistory = buildHistoryFromReviews(this.authStore.fullName, reviewLevels); @@ -160,6 +284,29 @@ export class FirebaseGeneReviewService { }); } + // We are deleting last because the indices will change after deleting from array. + let hasDeletion = false; + try { + // Todo: We should use multi-location updates for deletions once all our arrays use firebase auto-generated keys + // instead of using sequential number indices. + for (const pathType of [ + FIREBASE_LIST_PATH_TYPE.TREATMENT_LIST, + FIREBASE_LIST_PATH_TYPE.TUMOR_LIST, + FIREBASE_LIST_PATH_TYPE.MUTATION_LIST, + ]) { + for (const [firebasePath, deleteIndices] of Object.entries(itemsToDelete[pathType])) { + hasDeletion = true; + await this.firebaseRepository.deleteFromArray(firebasePath, deleteIndices); + } + } + // If user accepts a deletion individually, we need to refresh the ReviewPage with the latest data to make sure the indices are up to date. + if (reviewLevels.length === 1 && hasDeletion) { + return { shouldRefresh: true }; + } + } catch (error) { + throw new SentryError('Failed to accept deletions in review mode', { hugoSymbol, reviewLevels, isGermline, itemsToDelete }); + } + return this.processDeletion(reviewLevels.length, itemsToDelete); }; diff --git a/src/main/webapp/app/service/firebase/firebase-gene-service.ts b/src/main/webapp/app/service/firebase/firebase-gene-service.ts index 0c8783168..e4c37d20c 100644 --- a/src/main/webapp/app/service/firebase/firebase-gene-service.ts +++ b/src/main/webapp/app/service/firebase/firebase-gene-service.ts @@ -1,6 +1,8 @@ import { CancerType, DX_LEVELS, + Drug, + DrugCollection, FIREBASE_ONCOGENICITY, Gene, GenomicIndicator, @@ -10,23 +12,34 @@ import { TX_LEVELS, Treatment, Tumor, + Vus, } from 'app/shared/model/firebase/firebase.model'; import { isTxLevelPresent } from 'app/shared/util/firebase/firebase-level-utils'; import { extractArrayPath, parseFirebaseGenePath } from 'app/shared/util/firebase/firebase-path-utils'; import { FirebaseGeneReviewService } from 'app/service/firebase/firebase-gene-review-service'; -import { findNestedUuids, getFirebaseGenePath, isSectionRemovableWithoutReview } from 'app/shared/util/firebase/firebase-utils'; +import { + findNestedUuids, + getFirebaseGenePath, + getFirebaseVusPath, + isSectionRemovableWithoutReview, +} from 'app/shared/util/firebase/firebase-utils'; import AuthStore from '../../stores/authentication.store'; import { FirebaseRepository } from '../../stores/firebase/firebase-repository'; import { FirebaseMetaService } from './firebase-meta-service'; -import { ALLELE_STATE, PATHOGENIC_VARIANTS } from 'app/config/constants/firebase'; +import { ALLELE_STATE, FB_COLLECTION, PATHOGENIC_VARIANTS } from 'app/config/constants/firebase'; import { generateUuid, isPromiseOk } from 'app/shared/util/utils'; import { notifyError } from 'app/oncokb-commons/components/util/NotificationUtils'; import { getErrorMessage } from 'app/oncokb-commons/components/alert/ErrorAlertUtils'; import { FirebaseDataStore } from 'app/stores/firebase/firebase-data.store'; import { getTumorNameUuid, getUpdatedReview } from 'app/shared/util/firebase/firebase-review-utils'; import { SentryError } from 'app/config/sentry-error'; -import { GERMLINE_PATH } from 'app/config/constants/constants'; +import { GERMLINE_PATH, GET_ALL_DRUGS_PAGE_SIZE } from 'app/config/constants/constants'; import _ from 'lodash'; +import { getDriveAnnotations } from 'app/shared/util/core-drive-annotation-submission/core-drive-annotation-submission'; +import { DriveAnnotationApi } from 'app/shared/api/manual/drive-annotation-api'; +import GeneStore from 'app/entities/gene/gene.store'; +import { geneIsReleased } from 'app/shared/util/entity-utils/gene-entity-utils'; +import DrugStore from 'app/entities/drug/drug.store'; export type AllLevelSummary = { [mutationUuid: string]: { @@ -61,25 +74,34 @@ export type MutationLevelSummary = { export class FirebaseGeneService { firebaseRepository: FirebaseRepository; authStore: AuthStore; + geneStore: GeneStore; + drugStore: DrugStore; firebaseMutationListStore: FirebaseDataStore; firebaseMutationConvertIconStore: FirebaseDataStore; firebaseMetaService: FirebaseMetaService; firebaseGeneReviewService: FirebaseGeneReviewService; + driveAnnotationApi: DriveAnnotationApi; constructor( firebaseRepository: FirebaseRepository, authStore: AuthStore, + geneStore: GeneStore, + drugStore: DrugStore, firebaseMutationListStore: FirebaseDataStore, firebaseMutationConvertIconStore: FirebaseDataStore, firebaseMetaService: FirebaseMetaService, firebaseGeneReviewService: FirebaseGeneReviewService, + driveAnnotationApi: DriveAnnotationApi, ) { this.firebaseRepository = firebaseRepository; this.authStore = authStore; + this.geneStore = geneStore; + this.drugStore = drugStore; this.firebaseMutationListStore = firebaseMutationListStore; this.firebaseMutationConvertIconStore = firebaseMutationConvertIconStore; this.firebaseMetaService = firebaseMetaService; this.firebaseGeneReviewService = firebaseGeneReviewService; + this.driveAnnotationApi = driveAnnotationApi; } getAllLevelMutationSummaryStats = (mutations: Mutation[]) => { @@ -551,4 +573,65 @@ export class FirebaseGeneService { updateObject = async (path: string, value: any) => { await this.firebaseRepository.update(path, value); }; + + getDrugs = async () => { + const drugs = await this.drugStore.getEntities({ page: 0, size: GET_ALL_DRUGS_PAGE_SIZE, sort: ['id,asc'] }); + return ( + drugs.data.reduce((acc, next) => { + acc[next.uuid] = { + uuid: next.uuid, + description: '', + priority: 0, + synonyms: next.nciThesaurus?.synonyms?.map(synonym => synonym.name) || [], + ncitCode: next.nciThesaurus?.code || '', + ncitName: next.nciThesaurus?.displayName || '', + drugName: next.name, + }; + return acc; + }, {} as DrugCollection) || {} + ); + }; + + saveAllGenes = async (isGermlineProp: boolean) => { + const drugLookup = await this.getDrugs(); + const geneLookup = ((await this.firebaseRepository.get(getFirebaseGenePath(isGermlineProp))).val() as Record) ?? {}; + const vusLookup = + ((await this.firebaseRepository.get(getFirebaseVusPath(isGermlineProp))).val() as Record>) ?? {}; + let count = 0; + for (const [hugoSymbol, gene] of Object.entries(geneLookup)) { + count++; + // eslint-disable-next-line no-console + console.log(`${count} - Saving ${hugoSymbol}...`); + const nullableVus: Record | null = vusLookup[hugoSymbol]; + await this.saveGeneWithData(isGermlineProp, hugoSymbol, drugLookup, gene, nullableVus); + // eslint-disable-next-line no-console + console.log('\tDone Saving.'); + } + }; + + saveGene = async (isGermlineProp: boolean, hugoSymbolProp: string) => { + const drugLookup = await this.getDrugs(); + const nullableGene = (await this.firebaseRepository.get(getFirebaseGenePath(isGermlineProp, hugoSymbolProp))).val() as Gene | null; + const nullableVus = (await this.firebaseRepository.get(getFirebaseVusPath(isGermlineProp, hugoSymbolProp))).val() as Record< + string, + Vus + > | null; + await this.saveGeneWithData(isGermlineProp, hugoSymbolProp, drugLookup, nullableGene, nullableVus); + }; + saveGeneWithData = async ( + isGermlineProp: boolean, + hugoSymbolProp: string, + drugLookup: DrugCollection, + nullableGene: Gene | null, + nullableVus: Record | null, + ) => { + const searchResponse = await this.geneStore.searchEntities({ query: hugoSymbolProp, exact: true }); + const args: Parameters[1] = { + gene: nullableGene == null ? undefined : nullableGene, + vus: nullableVus == null ? undefined : Object.values(nullableVus), + releaseGene: searchResponse.data.some(gene => geneIsReleased(gene)), + }; + const driveAnnotation = getDriveAnnotations(drugLookup, args); + await this.driveAnnotationApi.submitDriveAnnotations(driveAnnotation); + }; } diff --git a/src/main/webapp/app/service/firebase/firebase-vus-service.ts b/src/main/webapp/app/service/firebase/firebase-vus-service.ts index ebece20e8..56fafad31 100644 --- a/src/main/webapp/app/service/firebase/firebase-vus-service.ts +++ b/src/main/webapp/app/service/firebase/firebase-vus-service.ts @@ -1,16 +1,18 @@ +import { EvidenceApi } from 'app/shared/api/manual/evidence-api'; import { Vus } from 'app/shared/model/firebase/firebase.model'; import { getUserFullName } from 'app/shared/util/utils'; import { AuthStore } from 'app/stores'; import { FirebaseRepository } from 'app/stores/firebase/firebase-repository'; -import { push } from 'firebase/database'; import _ from 'lodash'; export class FirebaseVusService { firebaseRepository: FirebaseRepository; + evidenceClient: EvidenceApi; authStore: AuthStore; - constructor(firebaseRepository: FirebaseRepository, authStore: AuthStore) { + constructor(firebaseRepository: FirebaseRepository, evidenceClient: EvidenceApi, authStore: AuthStore) { this.firebaseRepository = firebaseRepository; + this.evidenceClient = evidenceClient; this.authStore = authStore; } @@ -45,4 +47,8 @@ export class FirebaseVusService { deleteVus = async (path: string) => { await this.firebaseRepository.delete(path); }; + + async sendVusToCore(hugoSymbol: string, vus: Vus[]) { + await this.evidenceClient.updateVus(hugoSymbol, vus); + } } diff --git a/src/main/webapp/app/shared/api/clients.ts b/src/main/webapp/app/shared/api/clients.ts index 3f5b2d877..0e37d5270 100644 --- a/src/main/webapp/app/shared/api/clients.ts +++ b/src/main/webapp/app/shared/api/clients.ts @@ -11,6 +11,10 @@ import { CancerTypeResourceApi, ArticleResourceApi, } from './generated/curation/api'; +import { DriveAnnotationApi } from './manual/drive-annotation-api'; +import { EvidenceApi } from './manual/evidence-api'; +import { GeneTypeApi } from './manual/gene-type-api'; +import { GeneApi } from 'app/shared/api/manual/gene-api'; export const fdaSubmissionClient = new FdaSubmissionResourceApi(undefined, '', axiosInstance); export const geneClient = new GeneResourceApi(undefined, '', axiosInstance); @@ -22,3 +26,9 @@ export const flagClient = new FlagResourceApi(undefined, '', axiosInstance); export const associationClient = new AssociationResourceApi(undefined, '', axiosInstance); export const cancerTypeClient = new CancerTypeResourceApi(undefined, '', axiosInstance); export const articleClient = new ArticleResourceApi(undefined, '', axiosInstance); + +// The following are oncokb-core clients +export const evidenceClient = new EvidenceApi(undefined, '', axiosInstance); +export const geneTypeClient = new GeneTypeApi(undefined, '', axiosInstance); +export const driveAnnotationClient = new DriveAnnotationApi(undefined, '', axiosInstance); +export const geneLegacyApi = new GeneApi(undefined, '', axiosInstance); diff --git a/src/main/webapp/app/shared/api/manual/drive-annotation-api.ts b/src/main/webapp/app/shared/api/manual/drive-annotation-api.ts new file mode 100644 index 000000000..ac5eaedbc --- /dev/null +++ b/src/main/webapp/app/shared/api/manual/drive-annotation-api.ts @@ -0,0 +1,71 @@ +import { BASE_PATH, BaseAPI, RequestArgs } from '../generated/core/base'; +import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import { Configuration } from '../generated/core'; +import { createRequestFunction } from '../generated/core/common'; +import { DUMMY_BASE_URL, assertParamExists, serializeDataIfNeeded, setSearchParams, toPathString } from '../generated/core/common'; +import { getDriveAnnotations as getDriveAnnotation } from 'app/shared/util/core-drive-annotation-submission/core-drive-annotation-submission'; + +export const DriveAnnotationAxiosParamCreator = function (configuration?: Configuration) { + return { + // eslint-disable-next-line @typescript-eslint/require-await + async submitDriveAnnotations( + driveAnnotation: ReturnType, + options: AxiosRequestConfig = {}, + ): Promise { + // verify required parameter 'requestBody' is not null or undefined + assertParamExists('submitToDriveAnnotationsToCore', 'evidences', driveAnnotation); + const localVarPath = `/legacy-api/driveAnnotation`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions: { headers: any } | undefined = undefined; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + const headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + const bodyFormData = new URLSearchParams(); + for (const [key, value] of Object.entries(driveAnnotation)) { + if (typeof value === 'boolean') { + bodyFormData.append(key, `${value}`); + } else if (value !== undefined) { + bodyFormData.append(key, value); + } + } + localVarRequestOptions.data = bodyFormData; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +export const DriveAnnotationApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = DriveAnnotationAxiosParamCreator(configuration); + return { + async submitDriveAnnotations( + driveAnnotation: ReturnType, + options: AxiosRequestConfig = {}, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.submitDriveAnnotations(driveAnnotation, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + }; +}; + +export class DriveAnnotationApi extends BaseAPI { + public submitDriveAnnotations(driveAnnotation: ReturnType, options?: AxiosRequestConfig) { + return DriveAnnotationApiFp(this.configuration) + .submitDriveAnnotations(driveAnnotation, options) + .then(request => request(this.axios, this.basePath)); + } +} diff --git a/src/main/webapp/app/shared/api/manual/evidence-api.ts b/src/main/webapp/app/shared/api/manual/evidence-api.ts new file mode 100644 index 000000000..581857ef0 --- /dev/null +++ b/src/main/webapp/app/shared/api/manual/evidence-api.ts @@ -0,0 +1,149 @@ +import { getEvidence, pathToDeleteEvidenceArgs } from 'app/shared/util/core-evidence-submission/core-evidence-submission'; +import { BASE_PATH, BaseAPI, RequestArgs } from '../generated/core/base'; +import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import { Configuration } from '../generated/core'; +import { createRequestFunction } from '../generated/core/common'; +import { DUMMY_BASE_URL, assertParamExists, serializeDataIfNeeded, setSearchParams, toPathString } from '../generated/core/common'; +import { NonUndefined } from 'react-hook-form'; +import { Vus } from 'app/shared/model/firebase/firebase.model'; + +export const EvidenceAxiosParamCreator = function (configuration?: Configuration) { + return { + // eslint-disable-next-line @typescript-eslint/require-await + async submitEvidences(evidences: ReturnType, options: AxiosRequestConfig = {}): Promise { + // verify required parameter 'requestBody' is not null or undefined + assertParamExists('submitEvidences', 'evidences', evidences); + const localVarPath = `/legacy-api/evidences/update`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions: { headers: any } | undefined = undefined; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + const headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + localVarRequestOptions.data = serializeDataIfNeeded(evidences, localVarRequestOptions, configuration); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + + // eslint-disable-next-line @typescript-eslint/require-await + async deleteEvidences( + deleteEvidencesPayload: NonUndefined>, + options: AxiosRequestConfig = {}, + ): Promise { + // verify required parameter 'requestBody' is not null or undefined + assertParamExists('deleteEvidences', 'evidences', deleteEvidencesPayload); + const localVarPath = `/legacy-api/evidences/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions: { headers: any } | undefined = undefined; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + const headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + localVarRequestOptions.data = serializeDataIfNeeded(deleteEvidencesPayload, localVarRequestOptions, configuration); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + + // eslint-disable-next-line @typescript-eslint/require-await + async updateVus(hugoSymbol: string, vus: Vus[], options: AxiosRequestConfig = {}): Promise { + // verify required parameter 'requestBody' is not null or undefined + assertParamExists('updateVus', 'hugoSymbol', hugoSymbol); + assertParamExists('updateVus', 'vus', vus); + const localVarPath = `/legacy-api/vus/update/${hugoSymbol}`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions: { headers: any } | undefined = undefined; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + const headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + localVarRequestOptions.data = serializeDataIfNeeded(vus, localVarRequestOptions, configuration); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +export const EvidenceApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = EvidenceAxiosParamCreator(configuration); + return { + async submitEvidences( + evidences: ReturnType, + options: AxiosRequestConfig = {}, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.submitEvidences(evidences, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + async deleteEvidences( + deleteEvidencesPayload: NonUndefined>, + options: AxiosRequestConfig = {}, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteEvidences(deleteEvidencesPayload, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + async updateVus( + hugoSymbol: string, + vus: Vus[], + options: AxiosRequestConfig = {}, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateVus(hugoSymbol, vus, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + }; +}; + +export class EvidenceApi extends BaseAPI { + public submitEvidences(evidences: ReturnType, options?: AxiosRequestConfig) { + return EvidenceApiFp(this.configuration) + .submitEvidences(evidences, options) + .then(request => request(this.axios, this.basePath)); + } + public deleteEvidences(deleteEvidencesPayload: NonUndefined>, options?: AxiosRequestConfig) { + return EvidenceApiFp(this.configuration) + .deleteEvidences(deleteEvidencesPayload, options) + .then(request => request(this.axios, this.basePath)); + } + public updateVus(hugoSymbol: string, vus: Vus[], options?: AxiosRequestConfig) { + return EvidenceApiFp(this.configuration) + .updateVus(hugoSymbol, vus, options) + .then(request => request(this.axios, this.basePath)); + } +} diff --git a/src/main/webapp/app/shared/api/manual/gene-api.ts b/src/main/webapp/app/shared/api/manual/gene-api.ts new file mode 100644 index 000000000..2d8d45890 --- /dev/null +++ b/src/main/webapp/app/shared/api/manual/gene-api.ts @@ -0,0 +1,101 @@ +import { createGeneTypePayload, removeGenePayload } from 'app/shared/util/core-gene-type-submission/core-gene-type-submission'; +import { BASE_PATH, BaseAPI, RequestArgs } from '../generated/core/base'; +import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import { Configuration } from '../generated/core'; +import { createRequestFunction } from '../generated/core/common'; +import { DUMMY_BASE_URL, assertParamExists, serializeDataIfNeeded, setSearchParams, toPathString } from '../generated/core/common'; + +export const GeneAxiosParamCreator = function (configuration?: Configuration) { + return { + // eslint-disable-next-line @typescript-eslint/require-await + async submitGenes(genes: ReturnType, options: AxiosRequestConfig = {}): Promise { + // verify required parameter 'requestBody' is not null or undefined + assertParamExists('submitToGenesToCore', 'genes', genes); + const localVarPath = `/legacy-api/gene/update`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions: { headers: any } | undefined = undefined; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + const headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + localVarRequestOptions.data = serializeDataIfNeeded(genes, localVarRequestOptions, configuration); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + // eslint-disable-next-line @typescript-eslint/require-await + async removeGene(gene: ReturnType, options: AxiosRequestConfig = {}): Promise { + // verify required parameter 'requestBody' is not null or undefined + assertParamExists('removeGeneFromCore', 'gene', gene); + + const localVarPath = `/legacy-api/genes/remove/${gene.hugoSymbol}`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions: { headers: any } | undefined = undefined; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + const headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +export const GeneApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = GeneAxiosParamCreator(configuration); + return { + async submitGenes( + genes: ReturnType, + options: AxiosRequestConfig = {}, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.submitGenes(genes, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + async removeGene( + gene: ReturnType, + options: AxiosRequestConfig = {}, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.removeGene(gene, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + }; +}; + +export class GeneApi extends BaseAPI { + public submitGenes(genes: ReturnType, options?: AxiosRequestConfig) { + return GeneApiFp(this.configuration) + .submitGenes(genes, options) + .then(request => request(this.axios, this.basePath)); + } + + public removeGene(gene: ReturnType, options?: AxiosRequestConfig) { + return GeneApiFp(this.configuration) + .removeGene(gene, options) + .then(request => request(this.axios, this.basePath)); + } +} diff --git a/src/main/webapp/app/shared/api/manual/gene-type-api.ts b/src/main/webapp/app/shared/api/manual/gene-type-api.ts new file mode 100644 index 000000000..6a4a0dbd6 --- /dev/null +++ b/src/main/webapp/app/shared/api/manual/gene-type-api.ts @@ -0,0 +1,63 @@ +import { BASE_PATH, BaseAPI, RequestArgs } from '../generated/core/base'; +import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import { Configuration } from '../generated/core'; +import { createRequestFunction } from '../generated/core/common'; +import { DUMMY_BASE_URL, assertParamExists, serializeDataIfNeeded, setSearchParams, toPathString } from '../generated/core/common'; +import { createGeneTypePayload } from 'app/shared/util/core-gene-type-submission/core-gene-type-submission'; + +export const GeneTypeAxiosParamCreator = function (configuration?: Configuration) { + return { + // eslint-disable-next-line @typescript-eslint/require-await + async submitGeneTypeToCore( + geneTypePayload: ReturnType, + options: AxiosRequestConfig = {}, + ): Promise { + // verify required parameter 'requestBody' is not null or undefined + assertParamExists('submitGeneTypeToCore', 'geneTypePayload', geneTypePayload); + const localVarPath = `/legacy-api/genes/update/${geneTypePayload.hugoSymbol}`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions: { headers: any } | undefined = undefined; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + const headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + localVarRequestOptions.data = serializeDataIfNeeded(geneTypePayload, localVarRequestOptions, configuration); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +export const GeneTypeApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = GeneTypeAxiosParamCreator(configuration); + return { + async submitGeneTypeToCore( + geneType: ReturnType, + options: AxiosRequestConfig = {}, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.submitGeneTypeToCore(geneType, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + }; +}; + +export class GeneTypeApi extends BaseAPI { + public submitGeneTypeToCore(geneTypePayload: ReturnType, options?: AxiosRequestConfig) { + return GeneTypeApiFp(this.configuration) + .submitGeneTypeToCore(geneTypePayload, options) + .then(request => request(this.axios, this.basePath)); + } +} diff --git a/src/main/webapp/app/shared/button/SaveGeneButton.tsx b/src/main/webapp/app/shared/button/SaveGeneButton.tsx new file mode 100644 index 000000000..fce388f87 --- /dev/null +++ b/src/main/webapp/app/shared/button/SaveGeneButton.tsx @@ -0,0 +1,56 @@ +import { observer } from 'mobx-react'; +import { componentInject } from '../util/typed-inject'; +import { IRootStore } from 'app/stores'; +import React, { useCallback, useState } from 'react'; +import { ButtonProps } from 'reactstrap'; +import { AsyncSaveButton } from './AsyncSaveButton'; +import { notifyError, notifySuccess } from 'app/oncokb-commons/components/util/NotificationUtils'; +import { useDrugListRef } from '../util/utils'; + +type ISaveGeneButtonProps = StoreProps & { + hugoSymbol?: string; +} & ButtonProps & + Omit, 'onClick' | 'disabled'>; + +function SaveGeneButton({ hugoSymbol, firebaseGeneService, ...buttonProps }: ISaveGeneButtonProps) { + const [isSavePending, setIsSavePending] = useState(false); + const onClickHandler = useCallback(async () => { + setIsSavePending(true); + try { + const isGermline = false; + if (hugoSymbol === undefined) { + await firebaseGeneService?.saveAllGenes(isGermline); + notifySuccess('All genes saved!'); + } else { + await firebaseGeneService?.saveGene(isGermline, hugoSymbol); + notifySuccess(`${hugoSymbol} was saved!`); + } + } catch (e) { + console.error(e); + notifyError(e); + } + setIsSavePending(false); + }, [hugoSymbol]); + + const confirmText = hugoSymbol === undefined ? 'Save All Genes' : `Save ${hugoSymbol}`; + return ( + + ); +} + +const mapStoreToProps = ({ firebaseGeneService, drugStore }: IRootStore) => ({ + firebaseGeneService, + drugList: drugStore.entities, +}); + +type StoreProps = Partial>; + +export default componentInject(mapStoreToProps)(observer(SaveGeneButton)); diff --git a/src/main/webapp/app/shared/modal/AddVusModal.tsx b/src/main/webapp/app/shared/modal/AddVusModal.tsx index 4337ce3ad..0ea2b56a9 100644 --- a/src/main/webapp/app/shared/modal/AddVusModal.tsx +++ b/src/main/webapp/app/shared/modal/AddVusModal.tsx @@ -9,7 +9,7 @@ import { DuplicateMutationInfo, getDuplicateMutations, getFirebaseGenePath } fro import { componentInject } from '../util/typed-inject'; import { observer } from 'mobx-react'; import { IRootStore } from 'app/stores'; -import { onValue, ref } from 'firebase/database'; +import { onValue, ref, get } from 'firebase/database'; import { Button, Col, Row } from 'reactstrap'; import { GroupBase } from 'react-select'; import Select from 'react-select/dist/declarations/src/Select'; diff --git a/src/main/webapp/app/shared/model/firebase/firebase.model.ts b/src/main/webapp/app/shared/model/firebase/firebase.model.ts index c231392fd..7b6c2f771 100644 --- a/src/main/webapp/app/shared/model/firebase/firebase.model.ts +++ b/src/main/webapp/app/shared/model/firebase/firebase.model.ts @@ -1,4 +1,4 @@ -import { GERMLINE_INHERITANCE_MECHANISM, PATHOGENICITY, PENETRANCE } from 'app/config/constants/constants'; +import { GERMLINE_INHERITANCE_MECHANISM, MUTATION_EFFECT, PATHOGENICITY, PENETRANCE } from 'app/config/constants/constants'; import { ALLELE_STATE, GENE_TYPE, READABLE_FIELD } from 'app/config/constants/firebase'; import { AlterationTypeEnum, Gene as OncoKBGene } from 'app/shared/api/generated/curation'; import { generateUuid } from 'app/shared/util/utils'; @@ -97,6 +97,7 @@ export class Treatment { description_review?: Review; description_uuid: string = generateUuid(); indication = ''; + indication_review?: Review; indication_uuid: string = generateUuid(); level: TX_LEVELS = TX_LEVELS.LEVEL_EMPTY; level_review?: Review; @@ -241,7 +242,7 @@ export class MutationEffect { description = ''; description_review?: Review; description_uuid: string = generateUuid(); - effect = ''; + effect: MUTATION_EFFECT | 'None' | '' = ''; effect_review?: Review; effect_uuid: string = generateUuid(); oncogenic: FIREBASE_ONCOGENICITY | '' = ''; diff --git a/src/main/webapp/app/shared/table/VusTable.tsx b/src/main/webapp/app/shared/table/VusTable.tsx index 5a8928af3..521907275 100644 --- a/src/main/webapp/app/shared/table/VusTable.tsx +++ b/src/main/webapp/app/shared/table/VusTable.tsx @@ -21,7 +21,7 @@ import { observer } from 'mobx-react'; import { notifyError, notifySuccess } from 'app/oncokb-commons/components/util/NotificationUtils'; import classNames from 'classnames'; import AddButton from 'app/pages/curation/button/AddButton'; -import { onValue, ref } from 'firebase/database'; +import { get, onValue, ref } from 'firebase/database'; import { downloadFile } from 'app/shared/util/file-utils'; import { VusRecencyInfoIcon } from 'app/shared/icons/VusRecencyInfoIcon'; import DefaultBadge from 'app/shared/badge/DefaultBadge'; @@ -33,6 +33,7 @@ import MutationConvertIcon from '../icons/MutationConvertIcon'; import AddMutationModal from '../modal/AddMutationModal'; import { Unsubscribe } from 'firebase/database'; import { VUS_TABLE_ID } from 'app/config/constants/html-id'; +import { SentryError } from 'app/config/sentry-error'; export interface IVusTableProps extends StoreProps { hugoSymbol: string | undefined; @@ -56,6 +57,7 @@ const VusTable = ({ mutationsSectionRef, isGermline, account, + sendVusToCore, addVus, refreshVus, deleteVus, @@ -112,6 +114,7 @@ const VusTable = ({ try { if (vusData) { await refreshVus?.(`${firebaseVusPath}/${uuid}`, vusData[uuid]); + syncVusWithCore(); } } catch (error) { notifyError(error); @@ -121,6 +124,7 @@ const VusTable = ({ async function handleDelete() { try { await deleteVus?.(`${firebaseVusPath}/${currentActionVusUuid.current}`); + syncVusWithCore(); } catch (error) { notifyError(error); } @@ -129,6 +133,20 @@ const VusTable = ({ async function handleAddVus(variants: string[]) { await addVus?.(firebaseVusPath, variants); setShowAddVusModal(false); + syncVusWithCore(); + } + + async function syncVusWithCore() { + try { + if (firebaseDb && hugoSymbol) { + const firebaseVus = (await get(ref(firebaseDb, firebaseVusPath))).val() as Record; + const vus = Object.values(firebaseVus); + await sendVusToCore?.(hugoSymbol, vus); + } + } catch (e) { + const error = new SentryError('Fail to submit VUS data to core.', { exception: e, hugoSymbol }); + console.error(error); + } } const columns: SearchColumn[] = [ @@ -306,6 +324,7 @@ const mapStoreToProps = ({ fullName: authStore.fullName, addMutation: firebaseGeneService.addMutation, setOpenMutationCollapsibleIndex: openMutationCollapsibleStore.setOpenMutationCollapsibleIndex, + sendVusToCore: firebaseVusService.sendVusToCore.bind(firebaseVusService), }); type StoreProps = Partial>; diff --git a/src/main/webapp/app/shared/util/core-drive-annotation-submission/core-drive-annotation-submission.spec.ts b/src/main/webapp/app/shared/util/core-drive-annotation-submission/core-drive-annotation-submission.spec.ts new file mode 100644 index 000000000..e1524a926 --- /dev/null +++ b/src/main/webapp/app/shared/util/core-drive-annotation-submission/core-drive-annotation-submission.spec.ts @@ -0,0 +1,958 @@ +import { GENE_TYPE } from 'app/config/constants/firebase'; +import { + createMockCancerRisk, + createMockCancerType, + createMockComment, + createMockGene, + createMockGeneType, + createMockImplication, + createMockMutation, + createMockMutationEffect, + createMockMutationSpecificInheritanceMechanism, + createMockMutationSpecificPenetrance, + createMockReview, + createMockTi, + createMockTreatment, + createMockTumor, + createMockVus, + createMockVusTime, + createMockVusBy, +} from '../core-submission-shared/core-submission.mocks'; +import { DrugCollection, FDA_LEVELS, FIREBASE_ONCOGENICITY, TI_TYPE, TX_LEVELS, Vus } from 'app/shared/model/firebase/firebase.model'; +import { getGeneData, getVUSData } from './core-drive-annotation-submission'; +import { MUTATION_EFFECT } from 'app/config/constants/constants'; + +describe('Drive annotation to submit to core', () => { + describe('getGeneData', () => { + test('include only reviewed content', () => { + const geneData = getGeneData(mockGene, mockDrugList); + expect(geneData).toEqual(expectedGeneDataNoCommentsAndReviewed); + }); + }); + describe('getVusData', () => { + test('exclude comments', () => { + const vusData = getVUSData(mockVus); + const expectedVusData = [ + { name: 'F888I', time: { by: { email: 'doej@gmail.com', name: 'John Doe' }, value: 1680011347000 } }, + { name: 'F595I', time: { by: { email: 'doej@gmail.com', name: 'John Doe' }, value: 1680011347329 } }, + ]; + expect(vusData).toEqual(expectedVusData); + }); + }); +}); + +const mockVus: Vus[] = [ + createMockVus({ + name: 'F888I', + name_comments: [ + createMockComment({ + userName: 'doej', + content: 'comment', + date: '1720646502304', + }), + ], + time: createMockVusTime({ + by: createMockVusBy({ + name: 'John Doe', + email: 'doej@gmail.com', + }), + value: 1680011347000, + }), + }), + createMockVus({ + name: 'F595I', + name_comments: [ + createMockComment({ + userName: 'doej', + content: 'comment', + date: '1720646502305', + }), + ], + time: createMockVusTime({ + by: createMockVusBy({ + name: 'John Doe', + email: 'doej@gmail.com', + }), + value: 1680011347329, + }), + }), +]; + +const mockGene = createMockGene({ + background: 'BRAF background', + background_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'test', + }), + background_uuid: '16acb0f4-a01c-472e-b243-01560f92cf1e', + dmp_refseq_id: 'NM_000000.0', + dmp_refseq_id_grch38: 'NM_000000.0', + isoform_override: 'ENST00000000000', + isoform_override_grch38: 'ENST00000000000', + penetrance_uuid: 'fc7ae593-7075-42de-b90f-ee132238274b', + name: 'BRAF', + summary: 'BRAF Summary', + summary_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'test', + }), + summary_uuid: '616d02ba-6909-42b2-b145-f7b5627ad0a3', + type_uuid: 'ea4d7bd1-bc94-4877-987f-37f2f13e571b', + inheritanceMechanism_uuid: '99447f53-5908-4dd1-a6fd-732d5ccf0f62', + type: createMockGeneType({ + ocg_uuid: '1a233927-54d7-4921-97bf-08489a1e9c7d', + ocg: GENE_TYPE.ONCOGENE, + ocg_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: GENE_TYPE.TUMOR_SUPPRESSOR, + }), + tsg_uuid: 'aafdb835-106b-426a-89dc-970f49f7eee6', + tsg: GENE_TYPE.TUMOR_SUPPRESSOR, + tsg_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: GENE_TYPE.ONCOGENE, + }), + }), + mutations_uuid: '69e454db-db99-4979-a770-482f7937314b', + mutations: [ + createMockMutation({ + tumors_uuid: '8ba291c7-6565-45e8-9dfa-a185657886cc', + alterations_uuid: '145c01a5-ac80-4bdd-9169-e51da638b5f8', + mutation_effect: createMockMutationEffect({ + pathogenic_uuid: '23d48fd8-2b0f-4530-b5d9-aadf4ffd9325', + description: 'The class II BRAF', + description_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + lastReviewed: 'reviewed description', + }), + description_uuid: '5ac487cc-8f37-4542-a59c-7aaa31cde0fb', + effect: MUTATION_EFFECT.LIKELY_GAIN_OF_FUNCTION, + effect_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + lastReviewed: MUTATION_EFFECT.NEUTRAL, + }), + effect_uuid: '21371d1e-d93d-492d-b791-616714bcb3c3', + oncogenic: FIREBASE_ONCOGENICITY.LIKELY, + oncogenic_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + lastReviewed: FIREBASE_ONCOGENICITY.YES, + }), + oncogenic_uuid: '78d4f998-b73e-4535-9bb9-68cb72cf08cc', + short: '', + }), + mutation_effect_uuid: '77b1b627-a015-4d0c-8fda-b3eb1fc1ff2a', + name: 'L597S', + name_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + added: true, + }), + mutation_specific_cancer_risk: createMockCancerRisk({ + biallelic_uuid: 'aa3609a2-378f-4ff5-98f8-e57a032d56da', + mosaic_uuid: '50a2a7e4-58ee-4f96-8ad4-f431d2f1efa8', + monoallelic_uuid: '29a8eed7-0d6b-48d5-8e20-d73fc2c639f1', + }), + mutation_specific_inheritance_mechanism: createMockMutationSpecificInheritanceMechanism({ + description_uuid: '28f7e918-6c1b-45f5-accd-9cb6a7eb6149', + inheritanceMechanism_uuid: '59b3e3f9-5339-4bd1-8cc3-d97ca0c2509f', + }), + mutation_specific_penetrance: createMockMutationSpecificPenetrance({ + description_uuid: '531dc775-3c64-4605-9830-e00c92401f64', + penetrance_uuid: '494467d4-5d02-4bac-b654-ab7f7a88138d', + }), + name_uuid: 'b01b580f-d159-4c40-bf75-3c03704c2b3b', + }), + createMockMutation({ + tumors_uuid: '9672ea70-bde8-4149-80a5-d1983932b89b', + alterations_uuid: '724b958c-57c5-40e8-b330-ab0d25539a49', + mutation_effect: createMockMutationEffect({ + pathogenic_uuid: '11de51b2-1019-4ff7-95d0-9bcfe3439d2b', + description: 'The class II BRAF', + description_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + lastReviewed: 'reviewed description', + }), + description_uuid: 'b020228d-8bbf-4d1f-842f-dda76b4e6630', + effect: MUTATION_EFFECT.LIKELY_GAIN_OF_FUNCTION, + effect_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + lastReviewed: MUTATION_EFFECT.NEUTRAL, + }), + effect_uuid: 'bd08e589-0a0b-4ff9-b514-fe3fe1d84f30', + oncogenic: FIREBASE_ONCOGENICITY.LIKELY, + oncogenic_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + lastReviewed: FIREBASE_ONCOGENICITY.YES, + }), + oncogenic_uuid: 'f48ceb40-9fac-4681-90d7-163ed51dd6e2', + short: '', + }), + mutation_effect_uuid: 'f5646eb4-a8b2-4967-9f8e-fba706538a03', + name: 'L597S', + name_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + lastReviewed: 'reviewed name', + }), + mutation_specific_cancer_risk: createMockCancerRisk({ + biallelic_uuid: '763e8668-8db3-4092-a8fb-f2a71009127f', + mosaic_uuid: 'c7c43f59-45d6-4b07-91d1-f2595ff9f90a', + monoallelic_uuid: '3358cb78-3a48-41c0-a879-71611cd4dbad', + }), + mutation_specific_inheritance_mechanism: createMockMutationSpecificInheritanceMechanism({ + description_uuid: '59a97f8e-7fb9-4229-b08d-ed29347d8f22', + inheritanceMechanism_uuid: '635bcf72-cdc3-4ffd-97db-3e60b847def7', + }), + mutation_specific_penetrance: createMockMutationSpecificPenetrance({ + description_uuid: '4efb278a-8da4-4c4d-abd1-5f2bd463010c', + penetrance_uuid: 'd4d542a7-c942-47b6-bb44-4559fa742348', + }), + name_uuid: '90061776-866f-40c1-819a-00dbad8213d0', + summary: 'summary', + summary_uuid: 'dec9f8ee-7c5d-4740-b17c-86331d867b15', + }), + createMockMutation({ + mutation_effect: createMockMutationEffect({ + description: 'The class I', + description_review: createMockReview({ updateTime: 1717770480000, updatedBy: 'John Doe' }), + description_uuid: '84e58cf7-a4f0-4cd5-ac01-d8397526d0e7', + effect: MUTATION_EFFECT.GAIN_OF_FUNCTION, + effect_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + }), + effect_uuid: 'eea2c0e6-d8b0-4536-8df7-297ce756494c', + oncogenic: FIREBASE_ONCOGENICITY.YES, + oncogenic_review: createMockReview({ + updateTime: 1678309000000, + updatedBy: 'John Doe', + }), + oncogenic_uuid: 'f2dad6ab-f54c-4252-8d35-54116866d05c', + pathogenic_uuid: '688ebc31-f528-4f09-ae3d-ce5b935951ab', + short: '', + }), + mutation_effect_uuid: '3ef57442-f50e-4305-adc6-22fbce9a532d', + name: 'V600E', + name_review: createMockReview({ updateTime: 1717770480000, updatedBy: 'John Doe' }), + name_uuid: 'ee5900b8-d984-49a6-b6db-940eb1d0e807', + summary: 'summary', + summary_uuid: '8cbccd95-caf6-4a33-837d-5c72d1d221d7', + tumors_uuid: 'b9e062b0-a1ae-46ad-9fc6-5bb6ee6b3674', + alterations_uuid: '9eb7261b-74c8-463b-99cd-62815c416765', + mutation_specific_cancer_risk: createMockCancerRisk({ + biallelic_uuid: 'a0cf2c62-9e86-4cc6-accf-7d4b6dbc5fbc', + mosaic_uuid: '72e419cb-0d5e-4533-83cc-14c08df2a869', + monoallelic_uuid: '3d379ae4-eea1-47ba-a12c-b3f4cae4f78a', + }), + mutation_specific_inheritance_mechanism: createMockMutationSpecificInheritanceMechanism({ + description_uuid: '317a7a9e-b50f-4558-aadd-6a7a184e0f0b', + inheritanceMechanism_uuid: '65c4e130-703e-46b5-85a5-5bf751d630c1', + }), + mutation_specific_penetrance: createMockMutationSpecificPenetrance({ + description_uuid: 'b0a9729d-82b6-4f3a-ae5f-81a42c004f2f', + penetrance_uuid: '91308253-0677-44db-bf89-97d79d7ec98a', + }), + tumors: [ + createMockTumor({ + cancerTypes: [], + cancerTypes_review: createMockReview({ + added: true, + updateTime: 1717770480000, + updatedBy: 'John Doe', + }), + TIs: [], + diagnostic: createMockImplication({ + description: '', + description_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'description review', + }), + description_uuid: '5c0856a1-1a56-4f01-8620-9518ebfb233e', + level: '', + level_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'level review', + }), + level_uuid: '7afcbe32-1684-436e-b659-c37ada75bae2', + short: '', + excludedRCTs_uuid: '4a5ed610-72d6-4007-b68b-3913d9c01d38', + excludedRCTs_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + }), + }), + prognostic: { + description: '', + description_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'description review', + }), + description_uuid: 'b190d68a-bd23-47e8-a7e7-b67f91c946dd', + level: '', + level_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'level review', + }), + level_uuid: 'b12042a4-76c5-4029-97de-9722108b7b0e', + short: '', + excludedRCTs_uuid: '1cf4d012-1335-4229-8da1-76ced0819211', + excludedRCTs_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + }), + }, + prognosticSummary: '', + prognosticSummary_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'summary review', + }), + prognosticSummary_uuid: 'c961a5e9-8c02-4b32-b001-9fc37f6c60c8', + prognostic_uuid: 'a482d71c-0079-40bd-8d8b-a13d35e26fe3', + summary: 'Add cancer type test', + summary_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'summary review', + }), + summary_uuid: 'da67dbc0-7641-43db-a760-a3464e65a1c5', + diagnosticSummary_uuid: 'a601e863-3c0f-4c97-8037-b3aafd853aad', + diagnostic_uuid: 'c3d5a2d6-34ef-4645-9d8e-96739e4a3cd6', + cancerTypes_uuid: '553284ea-01f2-48c4-936c-2efae797d6f5', + excludedCancerTypes_uuid: '2b0384c4-5405-4478-bba8-0604bd0ef1b6', + }), + createMockTumor({ + TIs: [ + createMockTi({ + name: 'Standard implications for sensitivity to therapy', + name_uuid: 'b728d209-492b-41cd-aaa5-a16c61a770ed', + treatments_uuid: '6f8ed079-3b8b-437c-852c-c3c2a3c9a320', + treatments: [ + createMockTreatment({ + description: 'RAF inhibitor', + description_review: createMockReview({ updateTime: 1717770480000, updatedBy: 'John Doe' }), + description_uuid: 'd9c0bb27-9c2e-441a-abf3-9f0784943e1f', + fdaLevel: FDA_LEVELS.LEVEL_FDA2, + fdaLevel_review: createMockReview({ updateTime: 1717770480000, updatedBy: 'John Doe' }), + fdaLevel_uuid: '3cc835a2-523a-44ef-b703-2d9566c65717', + indication: 'FDA-approved', + indication_review: createMockReview({ updateTime: 1717770480000, updatedBy: 'John Doe' }), + indication_uuid: '6e5d7e6b-9fa6-442c-822b-863942e3ad34', + level: TX_LEVELS.LEVEL_1, + level_review: createMockReview({ updateTime: 1717770480000, updatedBy: 'John Doe' }), + level_uuid: '16d4d15a-d753-482c-ac56-74e9bd238ed1', + name: '0ade496c-77e6-45cd-8e96-7c7d87b4e5de + 001e534f-3e63-432f-90a6-d1af1759e4e2, 122b8921-3cd6-4b57-bdc8-5d39cc5465a1', + name_review: createMockReview({ updateTime: 1717770480000, updatedBy: 'John Doe' }), + name_uuid: '9b798839-592a-4cb0-a85a-099c2662c8ef', + propagation: TX_LEVELS.LEVEL_3B, + propagationLiquid: TX_LEVELS.LEVEL_NO, + propagationLiquid_uuid: '6f1085c4-98b6-4d91-a210-b9f078849bbf', + propagation_review: createMockReview({ updateTime: 1717770480000, updatedBy: 'John Doe' }), + propagation_uuid: 'be009470-ca82-47d3-9054-8b54701030cc', + short: 'Effective against brain metastases', + excludedRCTs_uuid: '4fc7b249-8ae1-49d4-864e-4fdf971e1a6c', + }), + createMockTreatment({ + description: 'Created test', + description_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'description review', + }), + description_uuid: '9f382ed6-237b-4c22-b772-0f67ed2f6bed', + fdaLevel: FDA_LEVELS.LEVEL_FDA2, + fdaLevel_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'fdaLevel review', + }), + fdaLevel_uuid: '0c32548e-4cee-48e5-b6c6-a438948c10bd', + indication: 'FDA-approved', + indication_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'indication Review', + }), + indication_uuid: 'bb2b3791-65c1-4b62-ac33-e64a5ee2af41', + level: TX_LEVELS.LEVEL_1, + level_uuid: '89948c0d-dba8-4393-b896-c30988cf817d', + level_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'level Review', + }), + name: '0ade496c-77e6-45cd-8e96-7c7d87b4e5de', + name_review: createMockReview({ + added: true, + updateTime: 1717770480000, + updatedBy: 'John Doe', + }), + name_uuid: '88acdec9-fd1b-4522-9c4c-b95c6ce9ae8a', + propagation: TX_LEVELS.LEVEL_1, + propagation_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + }), + propagationLiquid: TX_LEVELS.LEVEL_NO, + propagationLiquid_uuid: '09226040-8d58-47b9-aa69-dc1003694b33', + propagation_uuid: 'a500184d-f49d-4444-9da8-703b0ac474f3', + short: '', + excludedRCTs_uuid: '9ba9e897-8476-4304-af8a-a3289ee96052', + }), + createMockTreatment({ + description: 'inhibitor of V600-mutant BRAF', + description_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'description review', + }), + description_uuid: '45a0d045-43fd-47db-a0a9-1a86d65799c3', + fdaLevel: FDA_LEVELS.LEVEL_FDA2, + fdaLevel_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'fdaLevel review', + }), + fdaLevel_uuid: 'b05a26bf-c45a-4541-8007-197ecc2403a5', + indication: 'FDA-approved', + indication_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'indication Review', + }), + indication_uuid: '494cda87-150c-47e9-980c-6ce6409e0ed7', + level: TX_LEVELS.LEVEL_1, + level_uuid: '8876d3de-9e1,e-4d76-9909-3d6f1c30650c', + level_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'level Review', + }), + name: '0ade496c-77e6-45cd-8e96-7c7d87b4e5de, 122b8921-3cd6-4b57-bdc8-5d39cc5465a1', + name_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: '0ade496c-77e6-45cd-8e96-7c7d87b4e5de, 122b8921-3cd6-4b57-bdc8-5d39cc5465a1', + }), + name_uuid: '71cc9b4d-a636-4891-8d5d-56093a701280', + propagation: TX_LEVELS.LEVEL_3B, + propagation_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: TX_LEVELS.LEVEL_1, + }), + propagationLiquid: TX_LEVELS.LEVEL_NO, + propagationLiquid_uuid: '288e2f8e-2dd4-4771-ba8e-c63589640559', + propagation_uuid: '317bd394-7991-499e-a6a7-bcd415f821f6', + short: '', + excludedRCTs_uuid: '3a04990a-9b1f-4ceb-8896-4fd88ea2a02b', + }), + ], + type: TI_TYPE.SS, + }), + createMockTi({ + name: 'Standard implications for resistance to therapy', + name_uuid: '389ba0a6-26d4-4754-aa0f-190d4f72b177', + type: TI_TYPE.SR, + treatments_uuid: 'bc650bb2-65e7-4ca9-9b04-bc7961d8b135', + }), + createMockTi({ + name: 'Investigational implications for sensitivity to therapy', + name_uuid: '11d78588-9550-488a-a850-dd91e8614048', + type: TI_TYPE.IS, + treatments_uuid: '92b55ca1-8ef8-44d7-aa90-f10e6c1f7bb0', + }), + createMockTi({ + name: 'Investigational implications for resistance to therapy', + name_uuid: '748852c0-a8ec-4824-af73-aa698bb5ff2f', + type: TI_TYPE.IR, + treatments_uuid: '52878ba2-b2ba-4f20-b357-f8d5ac1eda2e', + }), + ], + cancerTypes: [ + createMockCancerType({ + code: 'MEL', + mainType: 'Melanoma', + subtype: 'Melanoma', + }), + ], + cancerTypes_uuid: '553284ea-01f2-48c4-936c-2efae797d6f5', + diagnostic: createMockImplication({ + description: '', + description_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'description review', + }), + description_uuid: '5c0856a1-1a56-4f01-8620-9518ebfb233e', + level: '', + level_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'level review', + }), + level_uuid: '7afcbe32-1684-436e-b659-c37ada75bae2', + short: '', + excludedRCTs: [], + excludedRCTs_uuid: '4a5ed610-72d6-4007-b68b-3913d9c01d38', + excludedRCTs_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + }), + }), + diagnosticSummary: '', + diagnosticSummary_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'summary review', + }), + diagnosticSummary_uuid: 'a601e863-3c0f-4c97-8037-b3aafd853aad', + diagnostic_uuid: 'c3d5a2d6-34ef-4645-9d8e-96739e4a3cd6', + excludedCancerTypes_uuid: '2b0384c4-5405-4478-bba8-0604bd0ef1b6', + prognostic: { + description: '', + description_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'description review', + }), + description_uuid: 'b190d68a-bd23-47e8-a7e7-b67f91c946dd', + excludedRCTs: [], + level: '', + level_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'level review', + }), + level_uuid: 'b12042a4-76c5-4029-97de-9722108b7b0e', + short: '', + excludedRCTs_uuid: '1cf4d012-1335-4229-8da1-76ced0819211', + excludedRCTs_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'reviewed excludedRCTs', + }), + }, + prognosticSummary: '', + prognosticSummary_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'summary review', + }), + prognosticSummary_uuid: 'c961a5e9-8c02-4b32-b001-9fc37f6c60c8', + prognostic_uuid: 'a482d71c-0079-40bd-8d8b-a13d35e26fe3', + summary: 'RAF-targeted inhibitors', + summary_review: createMockReview({ + updateTime: 1717770480000, + updatedBy: 'John Doe', + lastReviewed: 'summary review', + }), + summary_uuid: 'da67dbc0-7641-43db-a760-a3464e65a1c5', + }), + ], + }), + ], +}); + +const mockDrugList: DrugCollection = { + '001e534f-3e63-432f-90a6-d1af1759e4e2': { + description: '', + drugName: 'Encorafenib', + ncitCode: 'C98283', + ncitName: 'Encorafenib', + synonyms: ['LGX-818', 'LGX818', 'LGX 818', 'ENCORAFENIB', 'Braftovi'], + uuid: '001e534f-3e63-432f-90a6-d1af1759e4e2', + priority: 1, + }, + '0746dd92-8f4d-4bf7-9161-57a223cb67d5': { + description: '', + drugName: 'Pembrolizumab', + ncitCode: 'C106432', + ncitName: 'Pembrolizumab', + synonyms: [ + 'MK-3475', + 'PEMBROLIZUMAB', + 'Keytruda', + 'Lambrolizumab', + 'SCH 900475', + 'Immunoglobulin G4, Anti-(Human Programmed Cell Death 1); Humanized Mouse Monoclonal (228-L-proline(H10-S>P))gamma 4 Heavy Chain (134-218')-disulfide with Humanized Mouse Monoclonal Kappa Light Chain Dimer (226-226'':229-229'')-bisdisulfide', + ], + uuid: '0746dd92-8f4d-4bf7-9161-57a223cb67d5', + priority: 1, + }, + '0ade496c-77e6-45cd-8e96-7c7d87b4e5de': { + description: '', + drugName: 'MDM2 Antagonist RO5045337', + ncitCode: 'C91724', + ncitName: 'MDM2 Antagonist RO5045337', + synonyms: ['R7112', 'RO-5045337', 'RO5045337'], + uuid: '0ade496c-77e6-45cd-8e96-7c7d87b4e5de', + priority: 1, + }, + '0f991d49-4cf2-4975-b52f-d7d037aa7f11': { + description: '', + drugName: 'Nilotinib', + ncitCode: 'C48375', + ncitName: 'Nilotinib', + synonyms: [ + 'AMN 107 Base Form', + 'nilotinib', + '4-Methyl-3-((4-(3-pyridinyl)-2-pyrimidinyl)amino)-N-(5-(4-methyl-1H-imidazol-1-yl)-3-(trifluoromethyl)phenyl)benzamide', + 'NILOTINIB', + ], + uuid: '0f991d49-4cf2-4975-b52f-d7d037aa7f11', + priority: 1, + }, + '122b8921-3cd6-4b57-bdc8-5d39cc5465a1': { + description: '', + drugName: 'Ponatinib', + ncitCode: 'C95777', + ncitName: 'Ponatinib', + synonyms: [ + 'PONATINIB', + 'AP-24534', + 'AP24534', + 'Benzamide, 3-(2-Imidazo(1,2-B)Pyridazin-3-Ylethynyl)-4-Methyl-N-(4-((4-Methyl-1- Piperazinyl)Methyl)-3-(Trifluoromethyl)Phenyl)', + ], + uuid: '122b8921-3cd6-4b57-bdc8-5d39cc5465a1', + priority: 1, + }, +}; + +const expectedGeneDataNoCommentsAndReviewed = { + name: 'BRAF', + name_comments: [], + background: 'test', + background_uuid: '16acb0f4-a01c-472e-b243-01560f92cf1e', + dmp_refseq_id: 'NM_000000.0', + isoform_override: 'ENST00000000000', + mutations: [ + { + mutation_effect: { + description: 'reviewed description', + description_uuid: 'b020228d-8bbf-4d1f-842f-dda76b4e6630', + effect: 'Neutral', + effect_uuid: 'bd08e589-0a0b-4ff9-b514-fe3fe1d84f30', + oncogenic: 'Yes', + oncogenic_uuid: 'f48ceb40-9fac-4681-90d7-163ed51dd6e2', + short: '', + pathogenic: '', + pathogenic_uuid: '11de51b2-1019-4ff7-95d0-9bcfe3439d2b', + description_review: { updatedBy: 'John Doe', updateTime: 1678309000000 }, + effect_review: { updatedBy: 'John Doe', updateTime: 1678309000000 }, + oncogenic_review: { updatedBy: 'John Doe', updateTime: 1678309000000 }, + }, + mutation_effect_uuid: 'f5646eb4-a8b2-4967-9f8e-fba706538a03', + mutation_effect_comments: [], + name: 'reviewed name', + alterations: [], + alterations_uuid: '724b958c-57c5-40e8-b330-ab0d25539a49', + name_uuid: '90061776-866f-40c1-819a-00dbad8213d0', + tumors: [], + tumors_uuid: '9672ea70-bde8-4149-80a5-d1983932b89b', + mutation_specific_penetrance: { + penetrance: '', + penetrance_uuid: 'd4d542a7-c942-47b6-bb44-4559fa742348', + description: '', + description_uuid: '4efb278a-8da4-4c4d-abd1-5f2bd463010c', + }, + mutation_specific_inheritance_mechanism: { + inheritanceMechanism: '', + inheritanceMechanism_uuid: '635bcf72-cdc3-4ffd-97db-3e60b847def7', + description: '', + description_uuid: '59a97f8e-7fb9-4229-b08d-ed29347d8f22', + }, + mutation_specific_cancer_risk: { + monoallelic: '', + monoallelic_uuid: '3358cb78-3a48-41c0-a879-71611cd4dbad', + biallelic: '', + biallelic_uuid: '763e8668-8db3-4092-a8fb-f2a71009127f', + mosaic: '', + mosaic_uuid: 'c7c43f59-45d6-4b07-91d1-f2595ff9f90a', + }, + name_review: { updatedBy: 'John Doe', updateTime: 1678309000000 }, + summary: 'summary', + summary_uuid: 'dec9f8ee-7c5d-4740-b17c-86331d867b15', + }, + { + mutation_effect: { + description: 'The class I', + description_uuid: '84e58cf7-a4f0-4cd5-ac01-d8397526d0e7', + effect: 'Gain-of-function', + effect_uuid: 'eea2c0e6-d8b0-4536-8df7-297ce756494c', + oncogenic: 'Yes', + oncogenic_uuid: 'f2dad6ab-f54c-4252-8d35-54116866d05c', + short: '', + pathogenic: '', + pathogenic_uuid: '688ebc31-f528-4f09-ae3d-ce5b935951ab', + description_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + effect_review: { updatedBy: 'John Doe', updateTime: 1678309000000 }, + oncogenic_review: { updatedBy: 'John Doe', updateTime: 1678309000000 }, + }, + mutation_effect_uuid: '3ef57442-f50e-4305-adc6-22fbce9a532d', + mutation_effect_comments: [], + name: 'V600E', + alterations: [], + alterations_uuid: '9eb7261b-74c8-463b-99cd-62815c416765', + name_uuid: 'ee5900b8-d984-49a6-b6db-940eb1d0e807', + summary: 'summary', + summary_uuid: '8cbccd95-caf6-4a33-837d-5c72d1d221d7', + tumors: [ + { + TIs: [ + { + name: 'Standard implications for sensitivity to therapy', + name_uuid: 'b728d209-492b-41cd-aaa5-a16c61a770ed', + treatments: [ + { + description: 'RAF inhibitor', + description_uuid: 'd9c0bb27-9c2e-441a-abf3-9f0784943e1f', + indication: 'FDA-approved', + indication_uuid: '6e5d7e6b-9fa6-442c-822b-863942e3ad34', + level: '1', + level_uuid: '16d4d15a-d753-482c-ac56-74e9bd238ed1', + fdaLevel: 'Fda2', + fdaLevel_uuid: '3cc835a2-523a-44ef-b703-2d9566c65717', + name: [ + [ + { + description: '', + drugName: 'MDM2 Antagonist RO5045337', + ncitCode: 'C91724', + ncitName: 'MDM2 Antagonist RO5045337', + synonyms: ['R7112', 'RO-5045337', 'RO5045337'], + uuid: '0ade496c-77e6-45cd-8e96-7c7d87b4e5de', + priority: 1, + }, + { + description: '', + drugName: 'Encorafenib', + ncitCode: 'C98283', + ncitName: 'Encorafenib', + synonyms: ['LGX-818', 'LGX818', 'LGX 818', 'ENCORAFENIB', 'Braftovi'], + uuid: '001e534f-3e63-432f-90a6-d1af1759e4e2', + priority: 1, + }, + ], + [ + { + description: '', + drugName: 'Ponatinib', + ncitCode: 'C95777', + ncitName: 'Ponatinib', + synonyms: [ + 'PONATINIB', + 'AP-24534', + 'AP24534', + 'Benzamide, 3-(2-Imidazo(1,2-B)Pyridazin-3-Ylethynyl)-4-Methyl-N-(4-((4-Methyl-1- Piperazinyl)Methyl)-3-(Trifluoromethyl)Phenyl)', + ], + uuid: '122b8921-3cd6-4b57-bdc8-5d39cc5465a1', + priority: 1, + }, + ], + ], + name_comments: [], + name_uuid: '9b798839-592a-4cb0-a85a-099c2662c8ef', + propagation: '3B', + propagationLiquid: 'no', + propagationLiquid_uuid: '6f1085c4-98b6-4d91-a210-b9f078849bbf', + propagation_uuid: 'be009470-ca82-47d3-9054-8b54701030cc', + excludedRCTs: [], + excludedRCTs_uuid: '4fc7b249-8ae1-49d4-864e-4fdf971e1a6c', + short: 'Effective against brain metastases', + description_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + fdaLevel_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + indication_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + level_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + name_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + propagation_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + }, + { + description: 'description review', + description_uuid: '45a0d045-43fd-47db-a0a9-1a86d65799c3', + indication: 'indication Review', + indication_uuid: '494cda87-150c-47e9-980c-6ce6409e0ed7', + level: 'level Review', + level_uuid: '8876d3de-9e1,e-4d76-9909-3d6f1c30650c', + fdaLevel: 'fdaLevel review', + fdaLevel_uuid: 'b05a26bf-c45a-4541-8007-197ecc2403a5', + name: [ + [ + { + description: '', + drugName: 'MDM2 Antagonist RO5045337', + ncitCode: 'C91724', + ncitName: 'MDM2 Antagonist RO5045337', + synonyms: ['R7112', 'RO-5045337', 'RO5045337'], + uuid: '0ade496c-77e6-45cd-8e96-7c7d87b4e5de', + priority: 1, + }, + ], + [ + { + description: '', + drugName: 'Ponatinib', + ncitCode: 'C95777', + ncitName: 'Ponatinib', + synonyms: [ + 'PONATINIB', + 'AP-24534', + 'AP24534', + 'Benzamide, 3-(2-Imidazo(1,2-B)Pyridazin-3-Ylethynyl)-4-Methyl-N-(4-((4-Methyl-1- Piperazinyl)Methyl)-3-(Trifluoromethyl)Phenyl)', + ], + uuid: '122b8921-3cd6-4b57-bdc8-5d39cc5465a1', + priority: 1, + }, + ], + ], + name_comments: [], + name_uuid: '71cc9b4d-a636-4891-8d5d-56093a701280', + propagation: '1', + propagationLiquid: 'no', + propagationLiquid_uuid: '288e2f8e-2dd4-4771-ba8e-c63589640559', + propagation_uuid: '317bd394-7991-499e-a6a7-bcd415f821f6', + excludedRCTs: [], + excludedRCTs_uuid: '3a04990a-9b1f-4ceb-8896-4fd88ea2a02b', + short: '', + description_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + fdaLevel_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + indication_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + level_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + name_review: { + updatedBy: 'John Doe', + updateTime: 1717770480000, + }, + propagation_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + }, + ], + treatments_uuid: '6f8ed079-3b8b-437c-852c-c3c2a3c9a320', + type: 'SS', + }, + { + name: 'Standard implications for resistance to therapy', + name_uuid: '389ba0a6-26d4-4754-aa0f-190d4f72b177', + treatments: [], + treatments_uuid: 'bc650bb2-65e7-4ca9-9b04-bc7961d8b135', + type: 'SR', + }, + { + name: 'Investigational implications for sensitivity to therapy', + name_uuid: '11d78588-9550-488a-a850-dd91e8614048', + treatments: [], + treatments_uuid: '92b55ca1-8ef8-44d7-aa90-f10e6c1f7bb0', + type: 'IS', + }, + { + name: 'Investigational implications for resistance to therapy', + name_uuid: '748852c0-a8ec-4824-af73-aa698bb5ff2f', + treatments: [], + treatments_uuid: '52878ba2-b2ba-4f20-b357-f8d5ac1eda2e', + type: 'IR', + }, + ], + cancerTypes: [{ code: 'MEL', mainType: 'Melanoma', subtype: 'Melanoma' }], + cancerTypes_uuid: '553284ea-01f2-48c4-936c-2efae797d6f5', + cancerTypes_comments: [], + excludedCancerTypes: [], + excludedCancerTypes_uuid: '2b0384c4-5405-4478-bba8-0604bd0ef1b6', + diagnostic: { + description: 'description review', + description_uuid: '5c0856a1-1a56-4f01-8620-9518ebfb233e', + level: 'level review', + level_uuid: '7afcbe32-1684-436e-b659-c37ada75bae2', + excludedRCTs: [], + excludedRCTs_uuid: '4a5ed610-72d6-4007-b68b-3913d9c01d38', + short: '', + description_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + level_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + excludedRCTs_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + }, + diagnosticSummary: 'summary review', + diagnosticSummary_uuid: 'a601e863-3c0f-4c97-8037-b3aafd853aad', + diagnostic_comments: [], + diagnostic_uuid: 'c3d5a2d6-34ef-4645-9d8e-96739e4a3cd6', + prognostic: { + description: 'description review', + description_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + description_uuid: 'b190d68a-bd23-47e8-a7e7-b67f91c946dd', + level: 'level review', + level_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + level_uuid: 'b12042a4-76c5-4029-97de-9722108b7b0e', + short: '', + excludedRCTs_uuid: '1cf4d012-1335-4229-8da1-76ced0819211', + excludedRCTs_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + excludedRCTs: [], + }, + prognosticSummary: 'summary review', + prognosticSummary_uuid: 'c961a5e9-8c02-4b32-b001-9fc37f6c60c8', + prognostic_comments: [], + prognostic_uuid: 'a482d71c-0079-40bd-8d8b-a13d35e26fe3', + summary: 'summary review', + summary_uuid: 'da67dbc0-7641-43db-a760-a3464e65a1c5', + diagnosticSummary_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + prognosticSummary_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + summary_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + }, + ], + tumors_uuid: 'b9e062b0-a1ae-46ad-9fc6-5bb6ee6b3674', + mutation_specific_penetrance: { + penetrance: '', + penetrance_uuid: '91308253-0677-44db-bf89-97d79d7ec98a', + description: '', + description_uuid: 'b0a9729d-82b6-4f3a-ae5f-81a42c004f2f', + }, + mutation_specific_inheritance_mechanism: { + inheritanceMechanism: '', + inheritanceMechanism_uuid: '65c4e130-703e-46b5-85a5-5bf751d630c1', + description: '', + description_uuid: '317a7a9e-b50f-4558-aadd-6a7a184e0f0b', + }, + mutation_specific_cancer_risk: { + monoallelic: '', + monoallelic_uuid: '3d379ae4-eea1-47ba-a12c-b3f4cae4f78a', + biallelic: '', + biallelic_uuid: 'a0cf2c62-9e86-4cc6-accf-7d4b6dbc5fbc', + mosaic: '', + mosaic_uuid: '72e419cb-0d5e-4533-83cc-14c08df2a869', + }, + name_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + }, + ], + mutations_uuid: '69e454db-db99-4979-a770-482f7937314b', + summary: 'test', + summary_uuid: '616d02ba-6909-42b2-b145-f7b5627ad0a3', + penetrance: '', + penetrance_uuid: 'fc7ae593-7075-42de-b90f-ee132238274b', + penetrance_comments: [], + inheritanceMechanism: '', + inheritanceMechanism_uuid: '99447f53-5908-4dd1-a6fd-732d5ccf0f62', + inheritanceMechanism_comments: [], + type: { + ocg: 'Tumor Suppressor', + ocg_uuid: '1a233927-54d7-4921-97bf-08489a1e9c7d', + tsg: 'Oncogene', + tsg_uuid: 'aafdb835-106b-426a-89dc-970f49f7eee6', + ocg_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + tsg_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + }, + type_uuid: 'ea4d7bd1-bc94-4877-987f-37f2f13e571b', + dmp_refseq_id_grch38: 'NM_000000.0', + isoform_override_grch38: 'ENST00000000000', + genomic_indicators: [], + background_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, + summary_review: { updatedBy: 'John Doe', updateTime: 1717770480000 }, +}; diff --git a/src/main/webapp/app/shared/util/core-drive-annotation-submission/core-drive-annotation-submission.ts b/src/main/webapp/app/shared/util/core-drive-annotation-submission/core-drive-annotation-submission.ts new file mode 100644 index 000000000..25b7ff153 --- /dev/null +++ b/src/main/webapp/app/shared/util/core-drive-annotation-submission/core-drive-annotation-submission.ts @@ -0,0 +1,153 @@ +import _ from 'lodash'; +import { Drug, Gene, Mutation, Review, Treatment, Tumor, Vus, DrugCollection } from '../../model/firebase/firebase.model'; +import { useLastReviewedOnly } from '../core-submission-shared/core-submission-utils'; + +export function getGeneData(geneData: Gene, drugList: DrugCollection): Gene | undefined { + const gene = useLastReviewedOnly(geneData); + if (gene === undefined) { + return undefined; + } + processData(gene, ['summary', 'background']); + processData(gene.type, ['tsg', 'ocg']); + const tempMutations: Mutation[] = []; + for (const mutation of gene.mutations ?? []) { + const tempTumors: Tumor[] = []; + if (shouldExclude(mutation.name_review)) { + tempMutations.push(mutation); + continue; + } + processData(mutation, ['name']); + processData(mutation.mutation_effect, ['oncogenic', 'effect', 'description']); + for (const tumor of mutation.tumors ?? []) { + if (shouldExclude(tumor.cancerTypes_review)) { + tempTumors.push(tumor); + continue; + } + // process tumor cancerTypes + processData(tumor, ['summary', 'diagnosticSummary', 'prognosticSummary']); + processData(tumor.diagnostic, ['level', 'description', 'excludedRCTs']); + processData(tumor.prognostic, ['level', 'description', 'excludedRCTs']); + for (const ti of tumor.TIs) { + type TempTreatment = Omit & { name: ReturnType | string }; + const tempTreatments: TempTreatment[] = []; + for (const treatment of ti.treatments ?? []) { + if (shouldExclude(treatment.name_review)) { + tempTreatments.push(treatment); + return undefined; + } + (treatment as TempTreatment).name = drugUuidToDrug(treatment.name, drugList); + processData(treatment, ['level', 'propagation', 'propagationLiquid', 'indication', 'description', 'fdaLevel']); + } + for (const item of tempTreatments) { + const index = ti.treatments.indexOf(item as Treatment); + if (index !== -1) { + ti.treatments.splice(index, 1); + } + } + } + } + for (const tumor of tempTumors) { + const index = mutation.tumors.indexOf(tumor); + if (index !== -1) { + mutation.tumors.splice(index, 1); + } + } + } + for (const mutation of tempMutations) { + const index = gene.mutations.indexOf(mutation); + if (index !== -1) { + gene.mutations.splice(index, 1); + } + } + return gene; +} + +function processData(data: T | undefined, keys: K[]) { + if (data !== undefined) { + for (const key of keys) { + delete data?.[key + '_comments']; + const reviewKey = key + '_review'; + const maybeReview: Review | null | undefined = data?.[reviewKey]; + if (data !== null && typeof maybeReview === 'object' && maybeReview !== null) { + if (maybeReview.added) { + delete data[key]; + } else if ('lastReviewed' in maybeReview) { + data[key] = maybeReview.lastReviewed as (T & object)[K]; + } + } + } + } +} + +export function getVUSData(vus: Vus[]) { + const vusData = _.cloneDeep(vus); + const vusDataArray: Vus[] = []; + for (const vusItem of vusData) { + delete vusItem.name_comments; + vusDataArray.push(vusItem); + } + return vusDataArray; +} + +function shouldExclude(reviewObj: Review | undefined) { + /* initialUpdate is true when it is the first time updating excludedRCTs field. + * Normally for string fields, we set lastReviewed: "" to empty string. + * The "lastReviewed" for excludedRCTs is empty array, which cannot be stored + * into firebase, so we created this initialUpdate field to denote that. + * */ + return reviewObj && (reviewObj.added === true || reviewObj.promotedToMutation === true || reviewObj.initialUpdate === true); +} + +function drugUuidToDrug(key: string | undefined, drugList: DrugCollection): Drug[][] | Record { + if (key !== undefined) { + const keys = therapyStrToArr(key); + return getDrugsByUuids(keys, drugList); + } else { + return {}; + } +} + +function therapyStrToArr(key: string) { + if (key) { + return key.split(',').map(function (element) { + return element + .trim() + .split('+') + .map(x => x.trim()); + }); + } else { + return []; + } +} + +function getDrugsByUuids(keys: string[][], drugList: DrugCollection): Drug[][] { + return keys.map(function (element) { + return element.map(function (key) { + if (!drugList[key]) { + throw new Error(`Cannot find drug for ${key}`); + } + return drugList[key]; + }); + }); +} + +export type DriveAnnotation = { gene: string | undefined; vus: string | undefined; releaseGene: boolean }; +export function getDriveAnnotations( + drugList: DrugCollection, + { gene, vus, releaseGene }: { gene: Gene | undefined; vus: Vus[] | undefined; releaseGene: boolean }, +): DriveAnnotation { + const params: DriveAnnotation = { gene: undefined, vus: undefined, releaseGene: false }; + if (gene !== undefined) { + const geneData = getGeneData(gene, drugList); + if (geneData !== undefined) { + params.gene = JSON.stringify(geneData); + } + } + if (vus !== undefined) { + params.vus = JSON.stringify(getVUSData(vus)); + } + if (releaseGene) { + params.releaseGene = releaseGene; + } + return params; +} diff --git a/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission-utils.ts b/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission-utils.ts new file mode 100644 index 000000000..36e61a01b --- /dev/null +++ b/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission-utils.ts @@ -0,0 +1,267 @@ +import { DX_LEVELS, FDA_LEVELS, Mutation, PX_LEVELS, TI, TX_LEVELS, Treatment, Tumor } from 'app/shared/model/firebase/firebase.model'; +import _ from 'lodash'; +import { ImplicationLevelOfEvidenceEnum } from '../../api/generated/core/api'; + +export const LEVEL_MAPPING: Record = { + '': 'NO', + '1': 'LEVEL_1', + '2': 'LEVEL_2', + '3A': 'LEVEL_3A', + '3B': 'LEVEL_3B', + '4': 'LEVEL_4', + R1: 'LEVEL_R1', + R2: 'LEVEL_R2', + no: 'NO', + Px1: 'LEVEL_Px1', + Px2: 'LEVEL_Px2', + Px3: 'LEVEL_Px3', + Dx1: 'LEVEL_Dx1', + Dx2: 'LEVEL_Dx2', + Dx3: 'LEVEL_Dx3', +}; +export const FDA_LEVEL_MAPPING: Record = { + no: 'NO', + Fda1: 'LEVEL_Fda1', + Fda2: 'LEVEL_Fda2', + Fda3: 'LEVEL_Fda3', + '': 'NO', +}; + +export function mostRecentItem(reviewObjs: string | any[], include?: boolean) { + let mostRecent = -1; + for (let i = 0; i < reviewObjs.length; i++) { + if (!include) { + // This is designed to handle the reviewObj with systematically set updatetime + // when 'include' equals true, it will use all reviewObj in the list + // otherwise, we will only use the reviewObj with updatedBy info. + if (!reviewObjs[i] || !reviewObjs[i].updatedBy) continue; + } + let currentItemTime: number | Date | undefined = undefined; + if (reviewObjs[i] && reviewObjs[i].updateTime) { + currentItemTime = new Date(reviewObjs[i].updateTime); + } + // we only continue to check if current item time is valid + if (currentItemTime instanceof Date && !isNaN(currentItemTime.getTime())) { + if (mostRecent < 0) { + mostRecent = i; + } else { + // reset mostRect time when current item time is closer + const mostRecentTime = new Date(reviewObjs[mostRecent].updateTime); + if (mostRecentTime < currentItemTime) { + mostRecent = i; + } + } + } + } + if (mostRecent < 0) { + return 0; + } + return mostRecent; +} + +export function validateTimeFormat(updateTime: string | number | Date | undefined): string { + let tempTime = updateTime !== undefined ? new Date(updateTime) : undefined; + if (tempTime instanceof Date && !isNaN(tempTime.getTime())) { + updateTime = tempTime.getTime(); + } else { + // handle the case of time stamp in string format + tempTime = new Date(Number(updateTime)); + if (tempTime instanceof Date && !isNaN(tempTime.getTime())) { + updateTime = tempTime.getTime(); + } else { + updateTime = new Date().getTime(); + } + } + return updateTime.toString(); +} + +export function getNewPriorities(list: Treatment[], unapprovedUuids: string | string[]) { + const priorities: Record> = {}; + let count = 1; + + if (!_.isArray(unapprovedUuids)) { + unapprovedUuids = []; + } + _.each(list, function (treatmentSec, __) { + const name = + treatmentSec.name_review && treatmentSec.name_review.lastReviewed ? treatmentSec.name_review.lastReviewed : treatmentSec.name; + const uuid = treatmentSec.name_uuid; + let notNewlyAdded = true; + if (treatmentSec.name_review && treatmentSec.name_review.added) { + notNewlyAdded = false; + } + if (_.isString(name) && (notNewlyAdded || unapprovedUuids.indexOf(uuid) !== -1)) { + priorities[uuid] = {}; + _.each(name.split(','), function (t) { + const treatment = t.trim(); + priorities[uuid][treatment] = count; + count++; + }); + } + }); + return priorities; +} + +type CollectUUIDsArgs = ( + | { + type: 'mutation'; + obj: Mutation; + } + | { + type: 'tumor'; + obj: Tumor; + } + | { + type: 'TI'; + obj: TI; + } + | { + type: 'treatment'; + obj: Treatment; + } +) & { + uuids?: string[]; + uuidType?: 'insideOnly' | 'evidenceOnly' | 'sectionOnly'; +}; + +/** + * Collects UUIDs from a given object based on its type and specified UUID type. + * + * @param {CollectUUIDsArgs} args - The arguments for the function. + * @param {'mutation'|'tumor'|'TI'|'treatment'} args.type - The type of the object. + * @param {Mutation|Tumor|TI|Treatment} args.obj - The object to collect UUIDs from. + * @param {string[]} [args.uuids=[]] - The array to collect UUIDs into. + * @param {'insideOnly'|'evidenceOnly'|'sectionOnly'} [args.uuidType] - The type of UUIDs to collect. + * @returns {string[]} The array of collected UUIDs. + */ +export function collectUUIDs({ type, obj, uuids = [], uuidType }: CollectUUIDsArgs): string[] { + if (type === 'mutation') { + switch (uuidType) { + case 'insideOnly': + uuids.push(obj.mutation_effect.oncogenic_uuid); + uuids.push(obj.mutation_effect.effect_uuid); + uuids.push(obj.mutation_effect.description_uuid); + break; + case 'evidenceOnly': + uuids.push(obj.mutation_effect.oncogenic_uuid); + uuids.push(obj.mutation_effect.effect_uuid); + break; + case 'sectionOnly': + uuids.push(obj.name_uuid); + uuids.push(obj.mutation_effect_uuid); + break; + default: + uuids.push(obj.name_uuid); + uuids.push(obj.mutation_effect_uuid); + uuids.push(obj.mutation_effect.oncogenic_uuid); + uuids.push(obj.mutation_effect.effect_uuid); + uuids.push(obj.mutation_effect.description_uuid); + break; + } + _.each(obj.tumors, function (tumor) { + collectUUIDs({ type: 'tumor', obj: tumor, uuids, uuidType }); + }); + } else if (type === 'tumor') { + switch (uuidType) { + case 'insideOnly': + uuids.push(obj.summary_uuid); + uuids.push(obj.prognosticSummary_uuid); + uuids.push(obj.diagnosticSummary_uuid); + uuids.push(obj.prognostic.level_uuid); + uuids.push(obj.prognostic.description_uuid); + if (obj.prognostic.excludedRCTs_uuid) { + uuids.push(obj.prognostic.excludedRCTs_uuid); + } + uuids.push(obj.diagnostic.level_uuid); + uuids.push(obj.diagnostic.description_uuid); + if (obj.diagnostic.excludedRCTs_uuid) { + uuids.push(obj.diagnostic.excludedRCTs_uuid); + } + break; + case 'evidenceOnly': + uuids.push(obj.summary_uuid); + uuids.push(obj.prognosticSummary_uuid); + uuids.push(obj.diagnosticSummary_uuid); + uuids.push(obj.prognostic_uuid); + uuids.push(obj.diagnostic_uuid); + break; + case 'sectionOnly': + uuids.push(getTumorUuids(obj)); + uuids.push(obj.prognostic_uuid); + uuids.push(obj.diagnostic_uuid); + break; + default: + uuids.push(getTumorUuids(obj)); + uuids.push(obj.summary_uuid); + uuids.push(obj.prognosticSummary_uuid); + uuids.push(obj.diagnosticSummary_uuid); + uuids.push(obj.prognostic.level_uuid); + uuids.push(obj.prognostic.description_uuid); + if (obj.prognostic.excludedRCTs_uuid) { + uuids.push(obj.prognostic.excludedRCTs_uuid); + } + uuids.push(obj.diagnostic.level_uuid); + uuids.push(obj.diagnostic.description_uuid); + if (obj.diagnostic.excludedRCTs_uuid) { + uuids.push(obj.diagnostic.excludedRCTs_uuid); + } + break; + } + _.each(obj.TIs, function (ti) { + collectUUIDs({ type: 'TI', obj: ti, uuids, uuidType }); + }); + } else if (type === 'TI') { + switch (uuidType) { + case 'insideOnly': + case 'evidenceOnly': + break; + case 'sectionOnly': + uuids.push(obj.name_uuid); + break; + default: + uuids.push(obj.name_uuid); + break; + } + _.each(obj.treatments, function (treatment) { + collectUUIDs({ type: 'treatment', obj: treatment, uuids, uuidType }); + }); + } else if (type === 'treatment') { + switch (uuidType) { + case 'insideOnly': + uuids.push(obj.level_uuid); + uuids.push(obj.fdaLevel_uuid); + uuids.push(obj.propagation_uuid); + uuids.push(obj.propagationLiquid_uuid); + uuids.push(obj.indication_uuid); + uuids.push(obj.description_uuid); + break; + case 'evidenceOnly': + uuids.push(obj.name_uuid); + break; + case 'sectionOnly': + uuids.push(obj.name_uuid); + break; + default: + uuids.push(obj.name_uuid); + uuids.push(obj.level_uuid); + uuids.push(obj.fdaLevel_uuid); + uuids.push(obj.propagation_uuid); + uuids.push(obj.propagationLiquid_uuid); + uuids.push(obj.indication_uuid); + uuids.push(obj.description_uuid); + break; + } + } + return uuids; +} + +function getTumorUuids(tumor: Tumor) { + const uuids: string[] = []; + if (tumor.cancerTypes_uuid) { + uuids.push(tumor.cancerTypes_uuid); + } + if (tumor.excludedCancerTypes_uuid) { + uuids.push(tumor.excludedCancerTypes_uuid); + } + return uuids.join(','); +} diff --git a/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission.spec.ts b/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission.spec.ts new file mode 100644 index 000000000..61dfa2cc4 --- /dev/null +++ b/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission.spec.ts @@ -0,0 +1,1888 @@ +import { DX_LEVELS, FDA_LEVELS, FIREBASE_ONCOGENICITY, PX_LEVELS, TI_TYPE, TX_LEVELS } from 'app/shared/model/firebase/firebase.model'; +import { getEvidence, pathToDeleteEvidenceArgs, pathToGetEvidenceArgs } from './core-evidence-submission'; +import { + Evidence, + EvidenceEvidenceTypeEnum, + EvidenceFdaLevelEnum, + EvidenceLevelOfEvidenceEnum, + EvidenceLiquidPropagationLevelEnum, + EvidenceSolidPropagationLevelEnum, + TumorType, +} from 'app/shared/api/generated/core'; +import { generateUuid } from '../utils'; +import { + createMockCancerType, + createMockDrug, + createMockGene, + createMockImplication, + createMockMutation, + createMockMutationEffect, + createMockReview, + createMockTi, + createMockTreatment, + createMockTumor, +} from '../core-submission-shared/core-submission.mocks'; +import { MUTATION_EFFECT } from 'app/config/constants/constants'; + +type GetEvidenceArgs = Parameters[0]; +type GetEvidenceRtn = ReturnType; +describe('getEvidence to submit to core', () => { + describe('Path parse tests', () => { + const treatment = createMockTreatment({ + level: TX_LEVELS.LEVEL_1, + fdaLevel: FDA_LEVELS.LEVEL_FDA1, + propagation: TX_LEVELS.LEVEL_2, + propagationLiquid: TX_LEVELS.LEVEL_4, + description_review: createMockReview({}), + propagation_review: createMockReview({}), + excludedRCTs_review: createMockReview({}), + level_review: createMockReview({}), + name_review: createMockReview({}), + fdaLevel_review: createMockReview({}), + }); + const tiIs = createMockTi({ type: TI_TYPE.IS, treatments: [treatment] }); + const tiIr = createMockTi({ type: TI_TYPE.IR, treatments: [treatment] }); + const tiSs = createMockTi({ type: TI_TYPE.SS, treatments: [treatment] }); + const tiSr = createMockTi({ type: TI_TYPE.SR, treatments: [treatment] }); + + const tumor = createMockTumor({ + TIs: [tiIs, tiIr, tiSs, tiSr], + cancerTypes: [{ code: generateUuid(), subtype: generateUuid(), mainType: generateUuid() }], + excludedCancerTypes: [{ code: generateUuid(), subtype: generateUuid(), mainType: generateUuid() }], + prognostic: createMockImplication({ + level: DX_LEVELS.LEVEL_DX1, + excludedRCTs: [{ code: generateUuid(), subtype: generateUuid(), mainType: generateUuid() }], + }), + prognosticSummary_review: createMockReview({}), + + diagnostic: createMockImplication({ + level: PX_LEVELS.LEVEL_PX1, + excludedRCTs: [{ code: generateUuid(), subtype: generateUuid(), mainType: generateUuid() }], + }), + diagnosticSummary_review: createMockReview({}), + summary: generateUuid(), + summary_review: createMockReview({}), + cancerTypes_review: createMockReview({}), + }); + const mutation = createMockMutation({ + tumors: [tumor], + name_review: createMockReview({}), + mutation_effect: createMockMutationEffect({ + effect_review: createMockReview({}), + oncogenic_review: createMockReview({}), + description_review: createMockReview({}), + pathogenic_review: createMockReview({}), + }), + }); + const gene = createMockGene({ + mutations: [mutation], + summary_review: createMockReview({}), + background_review: createMockReview({}), + }); + + const baseExpectedArgs: Pick = { + drugListRef: {}, + entrezGeneId: Math.random(), + updateTime: new Date().getTime(), + gene, + }; + + type ArrayElement = [Parameters[0] & { valuePath: string }, args: Partial | undefined]; + + const goodTests: ArrayElement[] = [ + [{ ...baseExpectedArgs, valuePath: 'mutations/0' }, undefined], + // gene type change + [{ ...baseExpectedArgs, valuePath: 'type' }, undefined], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/name' }, + { + ...baseExpectedArgs, + type: 'MUTATION_NAME_CHANGE', + mutation, + gene, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/summary' }, + { + ...baseExpectedArgs, + type: 'MUTATION_SUMMARY', + mutation, + gene, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/cancerTypes' }, + { + ...baseExpectedArgs, + type: 'TUMOR_NAME_CHANGE', + tumor, + mutation, + gene, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/excludedCancerTypes' }, + { + ...baseExpectedArgs, + type: 'TUMOR_NAME_CHANGE', + tumor, + mutation, + gene, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/TIs/0/treatments/0/name' }, + { + ...baseExpectedArgs, + type: 'TREATMENT_NAME_CHANGE', + tumor, + mutation, + gene, + ti: tiIs, + treatment, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'summary' }, + { ...baseExpectedArgs, type: EvidenceEvidenceTypeEnum.GeneSummary }, + ], + [ + { ...baseExpectedArgs, valuePath: 'background' }, + { ...baseExpectedArgs, type: EvidenceEvidenceTypeEnum.GeneBackground }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/mutation_effect/oncogenic' }, + { ...baseExpectedArgs, type: EvidenceEvidenceTypeEnum.Oncogenic, mutation }, + ], + [ + { + ...baseExpectedArgs, + valuePath: 'mutations/0/tumors/0/summary', + }, + { ...baseExpectedArgs, type: EvidenceEvidenceTypeEnum.TumorTypeSummary, tumor, mutation }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/prognosticSummary' }, + { + ...baseExpectedArgs, + type: EvidenceEvidenceTypeEnum.PrognosticSummary, + tumor, + mutation, + gene, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/diagnosticSummary' }, + { + ...baseExpectedArgs, + type: EvidenceEvidenceTypeEnum.DiagnosticSummary, + tumor, + mutation, + gene, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/mutation_effect/effect' }, + { ...baseExpectedArgs, type: EvidenceEvidenceTypeEnum.MutationEffect, mutation }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/mutation_effect/description' }, + { ...baseExpectedArgs, type: EvidenceEvidenceTypeEnum.MutationEffect, mutation }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/prognostic/level' }, + { + ...baseExpectedArgs, + type: EvidenceEvidenceTypeEnum.PrognosticImplication, + tumor, + mutation, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/diagnostic/level' }, + { + ...baseExpectedArgs, + type: EvidenceEvidenceTypeEnum.DiagnosticImplication, + tumor, + mutation, + }, + ], + ...[ + { + level: TX_LEVELS.LEVEL_1, + type: EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugSensitivity, + }, + { + level: TX_LEVELS.LEVEL_2, + type: EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugSensitivity, + }, + { + level: TX_LEVELS.LEVEL_3A, + type: EvidenceEvidenceTypeEnum.InvestigationalTherapeuticImplicationsDrugSensitivity, + }, + { + level: TX_LEVELS.LEVEL_3B, + type: EvidenceEvidenceTypeEnum.InvestigationalTherapeuticImplicationsDrugSensitivity, + }, + { + level: TX_LEVELS.LEVEL_4, + type: EvidenceEvidenceTypeEnum.InvestigationalTherapeuticImplicationsDrugSensitivity, + }, + { + level: TX_LEVELS.LEVEL_R1, + type: EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugResistance, + }, + { + level: TX_LEVELS.LEVEL_R2, + type: EvidenceEvidenceTypeEnum.InvestigationalTherapeuticImplicationsDrugResistance, + }, + ].map(({ level, type }): ArrayElement => { + const newGene = createMockGene({ + ...gene, + mutations: [ + createMockMutation({ + ...mutation, + tumors: [ + createMockTumor({ ...tumor, TIs: [createMockTi({ ...tiIs, treatments: [createMockTreatment({ ...treatment, level })] })] }), + ], + }), + ], + }); + return [ + { + ...baseExpectedArgs, + gene: newGene, + valuePath: 'mutations/0/tumors/0/TIs/0/treatments/0', + }, + { + ...baseExpectedArgs, + type, + tumor: newGene.mutations[0].tumors[0], + mutation: newGene.mutations[0], + gene: newGene, + ti: newGene.mutations[0].tumors[0].TIs[0], + treatment: newGene.mutations[0].tumors[0].TIs[0].treatments[0], + }, + ]; + }), + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/TIs/1/treatments/0/name' }, + { + ...baseExpectedArgs, + type: 'TREATMENT_NAME_CHANGE', + tumor, + mutation, + gene, + ti: tiIr, + treatment, + }, + ], + ...Object.keys( + createMockTreatment({ + name: undefined, + level: undefined, + short: undefined, + fdaLevel: undefined, + name_uuid: undefined, + indication: undefined, + level_uuid: undefined, + description: undefined, + name_review: undefined, + propagation: undefined, + excludedRCTs: undefined, + level_review: undefined, + fdaLevel_uuid: undefined, + name_comments: undefined, + fdaLevel_review: undefined, + indication_uuid: undefined, + description_uuid: undefined, + propagation_uuid: undefined, + excludedRCTs_uuid: undefined, + propagationLiquid: undefined, + description_review: undefined, + propagation_review: undefined, + excludedRCTs_review: undefined, + propagationLiquid_uuid: undefined, + }), + ).map((key): ArrayElement => { + const valuePath = `mutations/0/tumors/0/TIs/1/treatments/0/${key}`; + if (key === 'short' || key === 'indication') { + return [{ ...baseExpectedArgs, valuePath }, undefined]; + } else if (key === 'name') { + return [ + { ...baseExpectedArgs, valuePath }, + { + ...baseExpectedArgs, + type: 'TREATMENT_NAME_CHANGE', + tumor, + mutation, + ti: tiIr, + treatment, + }, + ]; + } + return [ + { + ...baseExpectedArgs, + valuePath, + }, + { + ...baseExpectedArgs, + type: EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugSensitivity, + tumor, + mutation, + ti: tiIr, + treatment, + }, + ]; + }), + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/TIs/2/treatments/0/fdaLevel' }, + { + ...baseExpectedArgs, + type: EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugSensitivity, + tumor, + mutation, + ti: tiSs, + treatment, + }, + ], + [ + { ...baseExpectedArgs, valuePath: 'mutations/0/tumors/0/TIs/3/treatments/0/fdaLevel' }, + { + ...baseExpectedArgs, + type: EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugSensitivity, + tumor, + mutation, + gene, + ti: tiSr, + treatment, + }, + ], + ]; + test.each(goodTests)('Path should parse as expected for path %s', (args, expected) => { + expect(pathToGetEvidenceArgs(args)).toEqual(expected); + }); + const badTests: [valuePath: string][] = [ + ['bad/path'], + ['mutations/0/bad'], + ['mutations/300'], + ['mutations/bad/0/mutation_effect/'], + ['/'], + [''], + ]; + test.each(badTests)('Should throw exception for path "%s"', (valuePath: string) => { + expect(() => pathToGetEvidenceArgs({ ...baseExpectedArgs, valuePath })).toThrow(Error); + }); + }); + describe('evidences mapping tests', () => { + type PathArgs = Parameters[0]; + const baseArgs: Omit = { + drugListRef: {}, + entrezGeneId: Math.random(), + updateTime: new Date().getTime(), + }; + const baseEvidence: Evidence = { + additionalInfo: null as unknown as undefined, + alterations: null as unknown as undefined, + articles: null as unknown as undefined, + description: null as unknown as undefined, + fdaLevel: null as unknown as undefined, + knownEffect: null as unknown as undefined, + levelOfEvidence: null as unknown as undefined, + liquidPropagationLevel: null as unknown as undefined, + solidPropagationLevel: null as unknown as undefined, + treatments: null as unknown as undefined, + cancerTypes: [], + excludedCancerTypes: [], + relevantCancerTypes: [], + } as unknown as Evidence; + + const getTimeFromDateString = (s: string) => new Date(s).getTime(); + const hugoSymbol = 'ABL1'; + + const goodTests: [PathArgs, args: GetEvidenceRtn][] = [ + [ + { + ...baseArgs, + valuePath: 'summary', + gene: createMockGene({ + name: hugoSymbol, + summary: 'Gene Summary', + summary_uuid: 'd7ea4dc5-383d-40b8-a51f-725d25c087d2', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2000-01-01'), + }), + }), + }, + { + ['d7ea4dc5-383d-40b8-a51f-725d25c087d2']: { + ...baseEvidence, + evidenceType: EvidenceEvidenceTypeEnum.GeneSummary, + description: 'Gene Summary', + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + lastEdit: getTimeFromDateString('2000-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'background', + gene: createMockGene({ + name: hugoSymbol, + background: 'Gene Background', + background_uuid: 'e9c7cc54-87a7-4903-9d0b-84a61977e669', + background_review: createMockReview({ + updateTime: getTimeFromDateString('2001-01-01'), + }), + }), + }, + { + ['e9c7cc54-87a7-4903-9d0b-84a61977e669']: { + ...baseEvidence, + evidenceType: EvidenceEvidenceTypeEnum.GeneBackground, + description: 'Gene Background', + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + lastEdit: getTimeFromDateString('2001-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/prognosticSummary', + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + prognosticSummary_uuid: 'd9c991e4-1c13-4251-bd5a-fac0344b3470', + prognosticSummary: 'Prognostic Summary', + prognosticSummary_review: createMockReview({ + lastReviewed: 'Prognostic Summary Last Reviewed', + updateTime: getTimeFromDateString('2002-01-01'), + }), + summary: 'Summary', + summary_review: createMockReview({ + lastReviewed: 'Summary Last Reviewed', + updateTime: getTimeFromDateString('2002-01-01'), + }), + }), + ], + }), + ], + }), + }, + { + ['d9c991e4-1c13-4251-bd5a-fac0344b3470']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.PrognosticSummary, + description: 'Prognostic Summary', + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + lastEdit: getTimeFromDateString('2002-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/diagnosticSummary', + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + diagnosticSummary_uuid: 'a8ba8d36-ee0b-4e9c-ad3b-49137c271a82', + diagnosticSummary: 'Diagnostic Summary', + diagnosticSummary_review: createMockReview({ + updateTime: getTimeFromDateString('2000-01-01'), + }), + }), + ], + }), + ], + }), + }, + { + ['a8ba8d36-ee0b-4e9c-ad3b-49137c271a82']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.DiagnosticSummary, + description: 'Diagnostic Summary', + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + lastEdit: getTimeFromDateString('2000-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/mutation_effect/effect', + updateTime: getTimeFromDateString('2000-01-01'), + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + mutation_effect: createMockMutationEffect({ + description: 'Mutation Effect', + effect_uuid: 'a7046e8d-af62-4284-a14d-fc145134529a', + oncogenic: FIREBASE_ONCOGENICITY.LIKELY, + effect: MUTATION_EFFECT.NEUTRAL, + effect_review: createMockReview({ + updateTime: getTimeFromDateString('2002-01-01'), + }), + oncogenic_uuid: '8b076b49-5baa-4a74-90f1-c115a65b19e5', + oncogenic_review: createMockReview({ + updateTime: getTimeFromDateString('2001-01-01'), + }), + }), + }), + ], + }), + }, + { + ['a7046e8d-af62-4284-a14d-fc145134529a']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.MutationEffect, + description: 'Mutation Effect', + knownEffect: MUTATION_EFFECT.NEUTRAL, + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + lastEdit: getTimeFromDateString('2002-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/mutation_effect/oncogenic', + updateTime: getTimeFromDateString('2000-01-01'), + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + mutation_effect: createMockMutationEffect({ + description: 'Mutation Effect', + effect_uuid: 'a7046e8d-af62-4284-a14d-fc145134529a', + oncogenic: FIREBASE_ONCOGENICITY.LIKELY, + effect: MUTATION_EFFECT.NEUTRAL, + effect_review: createMockReview({ + updateTime: getTimeFromDateString('2002-01-01'), + }), + oncogenic_uuid: '8b076b49-5baa-4a74-90f1-c115a65b19e5', + oncogenic_review: createMockReview({ + lastReviewed: FIREBASE_ONCOGENICITY.YES, + updateTime: getTimeFromDateString('2001-01-01'), + }), + }), + }), + ], + }), + }, + { + ['8b076b49-5baa-4a74-90f1-c115a65b19e5']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.Oncogenic, + knownEffect: FIREBASE_ONCOGENICITY.LIKELY, + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + lastEdit: getTimeFromDateString('2001-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/mutation_effect/oncogenic', + updateTime: getTimeFromDateString('2000-01-01'), + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + mutation_effect: createMockMutationEffect({ + description: 'Mutation Effect', + effect_uuid: 'a7046e8d-af62-4284-a14d-fc145134529a', + oncogenic: FIREBASE_ONCOGENICITY.LIKELY, + effect: MUTATION_EFFECT.NEUTRAL, + effect_review: createMockReview({ + updateTime: getTimeFromDateString('2002-01-01'), + }), + oncogenic_uuid: '8b076b49-5baa-4a74-90f1-c115a65b19e5', + oncogenic_review: createMockReview({ + updateTime: getTimeFromDateString('2001-01-01'), + }), + }), + }), + ], + }), + }, + { + ['8b076b49-5baa-4a74-90f1-c115a65b19e5']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.Oncogenic, + knownEffect: FIREBASE_ONCOGENICITY.LIKELY, + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + lastEdit: getTimeFromDateString('2001-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/prognostic/level', + updateTime: getTimeFromDateString('2003-01-01'), + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + prognostic_uuid: '8947bdb9-b77d-4fe4-a8a1-71dc7c41b1e4', + prognostic: createMockImplication({ + level: PX_LEVELS.LEVEL_PX1, + description: 'Prognostic Implication', + }), + }), + ], + }), + ], + }), + }, + { + ['8947bdb9-b77d-4fe4-a8a1-71dc7c41b1e4']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.PrognosticImplication, + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + description: 'Prognostic Implication', + levelOfEvidence: EvidenceLevelOfEvidenceEnum.LevelPx1, + lastEdit: getTimeFromDateString('2003-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/diagnostic/level', + updateTime: getTimeFromDateString('2004-01-01'), + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + diagnostic_uuid: '732e4ea5-0bf7-4795-a98e-4479ed19ab56', + diagnostic: createMockImplication({ + level: DX_LEVELS.LEVEL_DX2, + description: 'Diagnostic Implication', + }), + }), + ], + }), + ], + }), + }, + { + ['732e4ea5-0bf7-4795-a98e-4479ed19ab56']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.DiagnosticImplication, + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + description: 'Diagnostic Implication', + levelOfEvidence: EvidenceLevelOfEvidenceEnum.LevelDx2, + lastEdit: getTimeFromDateString('2004-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/TIs/0/treatments/0', + updateTime: getTimeFromDateString('2002-01-01'), + drugListRef: { + ['76c75f3b-364a-418c-8661-48768fb0742a']: createMockDrug({ + uuid: '76c75f3b-364a-418c-8661-48768fb0742a', + drugName: 'a', + ncitCode: 'ANcitCode', + ncitName: 'ANcitName', + priority: 1, + }), + ['8fbca1dc-0b71-47b1-8511-b5a5b8906616']: createMockDrug({ + uuid: '8fbca1dc-0b71-47b1-8511-b5a5b8906616', + drugName: 'b', + ncitCode: 'BNcitCode', + ncitName: 'BNcitName', + priority: 2, + }), + ['20329090-99ab-4769-8932-b93346331f57']: createMockDrug({ + uuid: '20329090-99ab-4769-8932-b93346331f57', + drugName: 'c', + ncitCode: 'CNcitCode', + ncitName: 'CNcitName', + priority: 3, + }), + }, + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + TIs: [ + createMockTi({ + type: TI_TYPE.IS, + treatments: [ + createMockTreatment({ + name_uuid: '992bb496-7f19-4144-8664-3123756ad520', + name: '76c75f3b-364a-418c-8661-48768fb0742a,8fbca1dc-0b71-47b1-8511-b5a5b8906616,20329090-99ab-4769-8932-b93346331f57', + description: 'TI IS', + fdaLevel: FDA_LEVELS.LEVEL_FDA1, + level: TX_LEVELS.LEVEL_1, + propagationLiquid: TX_LEVELS.LEVEL_4, + propagation: TX_LEVELS.LEVEL_2, + }), + ], + }), + ], + }), + ], + }), + ], + }), + }, + { + ['992bb496-7f19-4144-8664-3123756ad520']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugSensitivity, + description: 'TI IS', + fdaLevel: EvidenceLevelOfEvidenceEnum.LevelFda1, + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + knownEffect: 'Sensitive', + lastEdit: getTimeFromDateString('2002-01-01').toString(), + levelOfEvidence: EvidenceLevelOfEvidenceEnum.Level1, + liquidPropagationLevel: EvidenceLevelOfEvidenceEnum.Level4, + solidPropagationLevel: EvidenceLevelOfEvidenceEnum.Level2, + treatments: [ + { + approvedIndications: [], + drugs: [ + createMockDrug({ + uuid: '76c75f3b-364a-418c-8661-48768fb0742a', + drugName: 'a', + ncitCode: 'ANcitCode', + ncitName: 'ANcitName', + priority: 1, + }), + ], + priority: 1, + }, + { + approvedIndications: [], + drugs: [ + createMockDrug({ + uuid: '8fbca1dc-0b71-47b1-8511-b5a5b8906616', + drugName: 'b', + ncitCode: 'BNcitCode', + ncitName: 'BNcitName', + priority: 1, + }), + ], + priority: 2, + }, + { + approvedIndications: [], + drugs: [ + createMockDrug({ + uuid: '20329090-99ab-4769-8932-b93346331f57', + drugName: 'c', + ncitCode: 'CNcitCode', + ncitName: 'CNcitName', + priority: 1, + }), + ], + priority: 3, + }, + ], + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/TIs/0/treatments/0', + updateTime: getTimeFromDateString('2002-01-01'), + drugListRef: { + ['76c75f3b-364a-418c-8661-48768fb0742a']: createMockDrug({ + uuid: '76c75f3b-364a-418c-8661-48768fb0742a', + drugName: 'a', + ncitCode: 'ANcitCode', + ncitName: 'ANcitName', + priority: 1, + }), + }, + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + TIs: [ + createMockTi({ + type: TI_TYPE.IS, + treatments: [ + createMockTreatment({ + name_uuid: '992bb496-7f19-4144-8664-3123756ad520', + name: '76c75f3b-364a-418c-8661-48768fb0742a', + description: 'TI IS', + fdaLevel: FDA_LEVELS.LEVEL_FDA1, + level: TX_LEVELS.LEVEL_2, + level_review: createMockReview({ + lastReviewed: TX_LEVELS.LEVEL_1, + updatedBy: 'John Doe', + updateTime: getTimeFromDateString('2001-01-01'), + }), + propagationLiquid: TX_LEVELS.LEVEL_4, + propagation: TX_LEVELS.LEVEL_2, + propagation_review: createMockReview({ + lastReviewed: TX_LEVELS.LEVEL_4, + updatedBy: 'John Doe', + updateTime: getTimeFromDateString('2001-01-01'), + }), + }), + ], + }), + ], + }), + ], + }), + ], + }), + }, + { + ['992bb496-7f19-4144-8664-3123756ad520']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugSensitivity, + description: 'TI IS', + fdaLevel: EvidenceLevelOfEvidenceEnum.LevelFda1, + gene: { + entrezGeneId: baseArgs.entrezGeneId, + hugoSymbol, + }, + knownEffect: 'Sensitive', + lastEdit: getTimeFromDateString('2002-01-01').toString(), + levelOfEvidence: EvidenceLevelOfEvidenceEnum.Level2, + liquidPropagationLevel: EvidenceLevelOfEvidenceEnum.Level4, + solidPropagationLevel: EvidenceLevelOfEvidenceEnum.Level2, + treatments: [ + { + approvedIndications: [], + drugs: [ + createMockDrug({ + uuid: '76c75f3b-364a-418c-8661-48768fb0742a', + drugName: 'a', + ncitCode: 'ANcitCode', + ncitName: 'ANcitName', + priority: 1, + }), + ], + priority: 1, + }, + ], + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/summary', + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + summary_uuid: 'be434f6e-d6e9-4809-9951-b0aa89e7a32c', + summary: 'Tumor Type Summary', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2004-01-01'), + }), + }), + ], + }), + ], + }), + }, + { + ['be434f6e-d6e9-4809-9951-b0aa89e7a32c']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.TumorTypeSummary, + description: 'Tumor Type Summary', + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + lastEdit: getTimeFromDateString('2004-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/summary', + gene: createMockGene({ + name: hugoSymbol, + summary: '', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2004-01-01'), + lastReviewed: 'Ignore summary', + }), + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + summary_uuid: 'be434f6e-d6e9-4809-9951-b0aa89e7a32c', + summary: 'Tumor Type Summary', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2004-01-01'), + lastReviewed: 'Tumor Type Summary Old', + }), + }), + ], + }), + ], + }), + }, + { + ['be434f6e-d6e9-4809-9951-b0aa89e7a32c']: { + ...baseEvidence, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + evidenceType: EvidenceEvidenceTypeEnum.TumorTypeSummary, + description: 'Tumor Type Summary', + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + lastEdit: getTimeFromDateString('2004-01-01').toString(), + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/name', + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + mutation_effect: createMockMutationEffect({ + oncogenic_uuid: 'afd5c3a0-930e-4cce-8bd5-66577ebac0eb', + effect_uuid: 'ceac443e-dc39-4f62-9e05-45b800326e18', + }), + tumors: [ + createMockTumor({ + summary: 'Tumor Type Summary', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2004-01-01'), + }), + summary_uuid: 'a264a9d7-69d5-47b2-9734-09a860dd9d9b', + prognosticSummary_uuid: '081d7177-83e9-4dae-96a1-fa869aec4b52', + diagnosticSummary_uuid: '1d36e653-2805-4b23-affc-e4b60f60d24c', + diagnostic_uuid: '7cb9656b-383d-49ef-92eb-35af6803a6f4', + prognostic_uuid: '70d937cc-1bb7-4315-9d4f-9a97cf9d728b', + TIs: [ + createMockTi({ + name_uuid: 'd8a2f58b-f9f2-462e-98b6-88ea866c636e', + treatments: [ + createMockTreatment({ + name: '20329090-99ab-4769-8932-b93346331f57', + name_uuid: 'd43e1f83-be01-43c2-bc1a-c020d204fbb1', + name_review: createMockReview({ + updateTime: 0, + }), + description: 'Treatment Description', + fdaLevel: FDA_LEVELS.LEVEL_FDA1, + level: TX_LEVELS.LEVEL_4, + propagationLiquid: TX_LEVELS.LEVEL_3A, + propagation: TX_LEVELS.LEVEL_2, + }), + ], + }), + ], + }), + ], + }), + ], + }), + }, + { + // Mutation UUID's + ['afd5c3a0-930e-4cce-8bd5-66577ebac0eb']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + ['ceac443e-dc39-4f62-9e05-45b800326e18']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + // tumor UUID's + ['081d7177-83e9-4dae-96a1-fa869aec4b52']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + ['a264a9d7-69d5-47b2-9734-09a860dd9d9b']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + ['1d36e653-2805-4b23-affc-e4b60f60d24c']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + ['7cb9656b-383d-49ef-92eb-35af6803a6f4']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + ['70d937cc-1bb7-4315-9d4f-9a97cf9d728b']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + // Treatment UUID's + ['d43e1f83-be01-43c2-bc1a-c020d204fbb1']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/summary', + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + summary: 'summary', + summary_uuid: '73821fe9-27c4-46bf-a0c0-05a73cf5cf7b', + mutation_effect: createMockMutationEffect({ + oncogenic_uuid: 'afd5c3a0-930e-4cce-8bd5-66577ebac0eb', + effect_uuid: 'ceac443e-dc39-4f62-9e05-45b800326e18', + }), + tumors: [ + createMockTumor({ + summary: 'Tumor Type Summary', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2004-01-01'), + }), + summary_uuid: 'a264a9d7-69d5-47b2-9734-09a860dd9d9b', + prognosticSummary_uuid: '081d7177-83e9-4dae-96a1-fa869aec4b52', + diagnosticSummary_uuid: '1d36e653-2805-4b23-affc-e4b60f60d24c', + diagnostic_uuid: '7cb9656b-383d-49ef-92eb-35af6803a6f4', + prognostic_uuid: '70d937cc-1bb7-4315-9d4f-9a97cf9d728b', + TIs: [ + createMockTi({ + name_uuid: 'd8a2f58b-f9f2-462e-98b6-88ea866c636e', + treatments: [ + createMockTreatment({ + name: '20329090-99ab-4769-8932-b93346331f57', + name_uuid: 'd43e1f83-be01-43c2-bc1a-c020d204fbb1', + name_review: createMockReview({ + updateTime: 0, + }), + description: 'Treatment Description', + fdaLevel: FDA_LEVELS.LEVEL_FDA1, + level: TX_LEVELS.LEVEL_4, + propagationLiquid: TX_LEVELS.LEVEL_3A, + propagation: TX_LEVELS.LEVEL_2, + }), + ], + }), + ], + }), + ], + }), + ], + }), + }, + { + // Mutation Summary UUID + ['73821fe9-27c4-46bf-a0c0-05a73cf5cf7b']: { + ...baseEvidence, + description: 'summary', + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: 'MUTATION_SUMMARY', + alterations: [ + { + alteration: 'mutation', + gene: { + hugoSymbol: 'ABL1', + }, + }, + ], + lastEdit: null as unknown as undefined, + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/cancerTypes', + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + mutation_effect: createMockMutationEffect({ + oncogenic_uuid: 'afd5c3a0-930e-4cce-8bd5-66577ebac0eb', + effect_uuid: 'ceac443e-dc39-4f62-9e05-45b800326e18', + }), + tumors: [ + createMockTumor({ + summary: 'Tumor Type Summary', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2004-01-01'), + }), + summary_uuid: 'a264a9d7-69d5-47b2-9734-09a860dd9d9b', + prognosticSummary_uuid: '081d7177-83e9-4dae-96a1-fa869aec4b52', + diagnosticSummary_uuid: '1d36e653-2805-4b23-affc-e4b60f60d24c', + diagnostic_uuid: '7cb9656b-383d-49ef-92eb-35af6803a6f4', + prognostic_uuid: '70d937cc-1bb7-4315-9d4f-9a97cf9d728b', + cancerTypes: [ + createMockCancerType({ + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + }), + ], + excludedCancerTypes: [ + createMockCancerType({ + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + }), + ], + TIs: [ + createMockTi({ + name_uuid: 'd8a2f58b-f9f2-462e-98b6-88ea866c636e', + treatments: [ + createMockTreatment({ + name: '20329090-99ab-4769-8932-b93346331f57', + name_uuid: 'd43e1f83-be01-43c2-bc1a-c020d204fbb1', + name_review: createMockReview({ + updateTime: 0, + }), + description: 'Treatment Description', + fdaLevel: FDA_LEVELS.LEVEL_FDA1, + level: TX_LEVELS.LEVEL_4, + propagationLiquid: TX_LEVELS.LEVEL_3A, + propagation: TX_LEVELS.LEVEL_2, + }), + ], + }), + ], + }), + ], + }), + ], + }), + }, + { + // tumor UUID's + ['081d7177-83e9-4dae-96a1-fa869aec4b52']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + ['a264a9d7-69d5-47b2-9734-09a860dd9d9b']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + ['1d36e653-2805-4b23-affc-e4b60f60d24c']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + ['7cb9656b-383d-49ef-92eb-35af6803a6f4']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + ['70d937cc-1bb7-4315-9d4f-9a97cf9d728b']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + // Treatment UUID's + ['d43e1f83-be01-43c2-bc1a-c020d204fbb1']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/excludedCancerTypes', + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + mutation_effect: createMockMutationEffect({ + oncogenic_uuid: 'afd5c3a0-930e-4cce-8bd5-66577ebac0eb', + effect_uuid: 'ceac443e-dc39-4f62-9e05-45b800326e18', + }), + tumors: [ + createMockTumor({ + summary: 'Tumor Type Summary', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2004-01-01'), + }), + summary_uuid: 'a264a9d7-69d5-47b2-9734-09a860dd9d9b', + prognosticSummary_uuid: '081d7177-83e9-4dae-96a1-fa869aec4b52', + diagnosticSummary_uuid: '1d36e653-2805-4b23-affc-e4b60f60d24c', + diagnostic_uuid: '7cb9656b-383d-49ef-92eb-35af6803a6f4', + prognostic_uuid: '70d937cc-1bb7-4315-9d4f-9a97cf9d728b', + cancerTypes: [ + createMockCancerType({ + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + }), + ], + excludedCancerTypes: [ + createMockCancerType({ + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + }), + ], + TIs: [ + createMockTi({ + name_uuid: 'd8a2f58b-f9f2-462e-98b6-88ea866c636e', + treatments: [ + createMockTreatment({ + name: '20329090-99ab-4769-8932-b93346331f57', + name_uuid: 'd43e1f83-be01-43c2-bc1a-c020d204fbb1', + name_review: createMockReview({ + updateTime: 0, + }), + description: 'Treatment Description', + fdaLevel: FDA_LEVELS.LEVEL_FDA1, + level: TX_LEVELS.LEVEL_4, + propagationLiquid: TX_LEVELS.LEVEL_3A, + propagation: TX_LEVELS.LEVEL_2, + }), + ], + }), + ], + }), + ], + }), + ], + }), + }, + { + // tumor UUID's + ['081d7177-83e9-4dae-96a1-fa869aec4b52']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + ['a264a9d7-69d5-47b2-9734-09a860dd9d9b']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + ['1d36e653-2805-4b23-affc-e4b60f60d24c']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + ['7cb9656b-383d-49ef-92eb-35af6803a6f4']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + ['70d937cc-1bb7-4315-9d4f-9a97cf9d728b']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + // Treatment UUID's + ['d43e1f83-be01-43c2-bc1a-c020d204fbb1']: { + ...baseEvidence, + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + evidenceType: null as unknown as undefined, + lastEdit: null as unknown as undefined, + cancerTypes: [ + { + code: 'cancerTypeCode', + subtype: 'cancerTypeSubType', + mainType: 'cancerTypeMainType', + } as TumorType, + ], + excludedCancerTypes: [ + { + code: 'excludedCancerTypeCode', + subtype: 'excludedCancerTypeSubType', + mainType: 'excludedCancerTypeMainType', + } as TumorType, + ], + }, + }, + ], + [ + { + ...baseArgs, + valuePath: 'mutations/0/tumors/0/TIs/0/treatments/0/name', + drugListRef: { + ['76c75f3b-364a-418c-8661-48768fb0742a']: createMockDrug({ + uuid: '76c75f3b-364a-418c-8661-48768fb0742a', + drugName: 'a', + ncitCode: 'ANcitCode', + ncitName: 'ANcitName', + priority: 1, + }), + ['8fbca1dc-0b71-47b1-8511-b5a5b8906616']: createMockDrug({ + uuid: '8fbca1dc-0b71-47b1-8511-b5a5b8906616', + drugName: 'b', + ncitCode: 'BNcitCode', + ncitName: 'BNcitName', + priority: 2, + }), + ['20329090-99ab-4769-8932-b93346331f57']: createMockDrug({ + uuid: '20329090-99ab-4769-8932-b93346331f57', + drugName: 'c', + ncitCode: 'CNcitCode', + ncitName: 'CNcitName', + priority: 3, + }), + }, + gene: createMockGene({ + name: hugoSymbol, + mutations: [ + createMockMutation({ + tumors: [ + createMockTumor({ + summary: 'Tumor Type Summary', + summary_review: createMockReview({ + updateTime: getTimeFromDateString('2004-01-01'), + }), + TIs: [ + createMockTi({ + treatments: [ + createMockTreatment({ + name: '20329090-99ab-4769-8932-b93346331f57', + name_uuid: 'd43e1f83-be01-43c2-bc1a-c020d204fbb1', + name_review: createMockReview({ + updateTime: 0, + }), + description: 'Treatment Description', + fdaLevel: FDA_LEVELS.LEVEL_FDA1, + level: TX_LEVELS.LEVEL_4, + propagationLiquid: TX_LEVELS.LEVEL_3A, + propagation: TX_LEVELS.LEVEL_2, + }), + ], + }), + ], + }), + ], + }), + ], + }), + }, + { + ['d43e1f83-be01-43c2-bc1a-c020d204fbb1']: { + ...baseEvidence, + fdaLevel: EvidenceFdaLevelEnum.LevelFda1, + evidenceType: null as unknown as undefined, + description: 'Treatment Description', + gene: { entrezGeneId: baseArgs.entrezGeneId, hugoSymbol }, + lastEdit: baseArgs.updateTime.toString(), + levelOfEvidence: EvidenceLevelOfEvidenceEnum.Level4, + liquidPropagationLevel: EvidenceLiquidPropagationLevelEnum.Level3A, + solidPropagationLevel: EvidenceSolidPropagationLevelEnum.Level2, + treatments: [ + { + approvedIndications: [], + drugs: [ + createMockDrug({ + uuid: '20329090-99ab-4769-8932-b93346331f57', + drugName: 'c', + ncitCode: 'CNcitCode', + ncitName: 'CNcitName', + priority: 1, + }), + ], + priority: 1, + }, + ], + }, + }, + ], + ]; + test.each(goodTests)('Path should map to evidences as expected for path %s', (pathArgs, expected) => { + const args = pathToGetEvidenceArgs(pathArgs); + expect(getEvidence(args as GetEvidenceArgs)).toEqual(expected); + }); + }); +}); + +describe('pathToDeleteEvidenceArgs', () => { + const gene = createMockGene({ + mutations: [ + createMockMutation({ + mutation_effect: createMockMutationEffect({ + effect_uuid: '78676de8-1cfb-4865-b1ce-8bf757048f7b', + oncogenic_uuid: '4fb196f8-31ed-4834-9226-ccffa8b4fdd3', + pathogenic_uuid: 'a375d0bc-9d79-4522-bb12-de6b5545ba79', + description_uuid: '6f4e8643-d3db-4e18-bde8-fb75ac29a56b', + }), + tumors: [ + createMockTumor({ + summary_uuid: 'df486ee9-6af3-4cef-9394-e9cbdf4226eb', + diagnostic_uuid: 'a8b6f194-6ab4-4702-9977-6b3f534d4ef4u', + prognostic_uuid: '00fb8a39-3e87-4c6e-8d81-8c9c64855e13', + cancerTypes_uuid: 'd5ff8db6-d0b2-49f3-911a-ba4d934341cf', + diagnosticSummary_uuid: 'ab9dae25-cf83-42df-8975-4373e018d941', + prognosticSummary_uuid: '798a5092-0294-49aa-b6e9-f12705334fc0', + excludedCancerTypes_uuid: '2070f39a-86fa-4953-a2d1-d3ba9298fe7b', + diagnostic: createMockImplication({ + description_uuid: '44d840a1-a132-4a4f-a695-709e1cdf3333', + level_uuid: 'ebfbc714-cbb8-4ecd-ad8c-cc38f3b128b2', + excludedRCTs_uuid: '0a83cfc3-d7bf-455b-8ef3-a8c8956758ae', + }), + prognostic: createMockImplication({ + description_uuid: 'bfa1635e-3880-41b2-979a-aad43ddc06d4', + level_uuid: 'e49a683c-e124-410b-a9a4-1ac20843339b', + excludedRCTs_uuid: '8d0c2a8d-42e5-4714-b516-f6f282f42eea', + }), + TIs: [ + createMockTi({ + name_uuid: '985d587f-10bc-44a8-a260-5edc57ef0b70', + treatments_uuid: '68317e32-66ec-46c1-8f62-046813706031', + treatments: [ + createMockTreatment({ + name_uuid: 'a36ec6af-7a3d-48cf-b841-de7412b88ef5', + level_uuid: 'e46edf6e-ca0f-46ff-ac5c-e679389525e8', + description_uuid: 'd6b9d84e-c940-42b0-bb05-f24472db24d4', + indication_uuid: '82e7a319-c2ec-4001-8736-a39dda6dffd8', + propagation_uuid: 'bebe734a-ca9c-465a-9297-30bb6610f721', + excludedRCTs_uuid: 'f9635be0-900c-45f9-a192-4647e518996e', + fdaLevel_uuid: 'e6126f59-c2bb-49b0-95b2-45577fad6a7b', + propagationLiquid_uuid: '5f73012f-1eb2-4342-b40e-002d4219c3c5', + }), + ], + }), + ], + }), + ], + }), + ], + }); + const tests: [Parameters[0], Required>][] = [ + [ + { + valuePath: 'mutations/0', + gene, + }, + [ + '4fb196f8-31ed-4834-9226-ccffa8b4fdd3', + '78676de8-1cfb-4865-b1ce-8bf757048f7b', + 'df486ee9-6af3-4cef-9394-e9cbdf4226eb', + '798a5092-0294-49aa-b6e9-f12705334fc0', + 'ab9dae25-cf83-42df-8975-4373e018d941', + '00fb8a39-3e87-4c6e-8d81-8c9c64855e13', + 'a8b6f194-6ab4-4702-9977-6b3f534d4ef4u', + 'a36ec6af-7a3d-48cf-b841-de7412b88ef5', + ], + ], + [ + { + valuePath: 'mutations/0/tumors/0', + gene, + }, + [ + 'df486ee9-6af3-4cef-9394-e9cbdf4226eb', + '798a5092-0294-49aa-b6e9-f12705334fc0', + 'ab9dae25-cf83-42df-8975-4373e018d941', + '00fb8a39-3e87-4c6e-8d81-8c9c64855e13', + 'a8b6f194-6ab4-4702-9977-6b3f534d4ef4u', + 'a36ec6af-7a3d-48cf-b841-de7412b88ef5', + ], + ], + [ + { + valuePath: 'mutations/0/tumors/0/TIs/0/treatments/0', + gene, + }, + ['a36ec6af-7a3d-48cf-b841-de7412b88ef5'], + ], + [ + { + valuePath: 'name', + gene, + }, + undefined, + ], + ]; + + test.each(tests)('Should map to payload correctly %j', (args, expected) => { + expect(pathToDeleteEvidenceArgs(args)).toEqual(expected); + }); +}); diff --git a/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission.ts b/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission.ts new file mode 100644 index 000000000..fbc70e823 --- /dev/null +++ b/src/main/webapp/app/shared/util/core-evidence-submission/core-evidence-submission.ts @@ -0,0 +1,251 @@ +import _ from 'lodash'; +import { Drug, DrugCollection, Gene, Mutation, MutationEffect, TI, TX_LEVELS, Treatment, Tumor } from '../../model/firebase/firebase.model'; +import { resolveTypeSpecificData } from './type-specific-resolvers'; +import { FDA_LEVEL_MAPPING, LEVEL_MAPPING, collectUUIDs, getNewPriorities, validateTimeFormat } from './core-evidence-submission-utils'; +import { Alteration, Evidence, EvidenceEvidenceTypeEnum } from '../../api/generated/core/api'; + +export type GetEvidenceArgs = { + type: EvidenceEvidenceTypeEnum | 'MUTATION_NAME_CHANGE' | 'TUMOR_NAME_CHANGE' | 'TREATMENT_NAME_CHANGE'; + mutation: Mutation; + tumor: Tumor; + ti: TI; + treatment: Treatment; + gene: Gene; + entrezGeneId: number | undefined; + drugListRef: DrugCollection; + updateTime: number; +}; + +export type KnownEffect = MutationEffect['oncogenic'] | 'Resistant' | 'Sensitive'; + +export function pathToDeleteEvidenceArgs({ valuePath, gene }: { valuePath: string; gene: Gene }): string[] | undefined { + const { mutation, tumor, treatment } = extractObjsInValuePath(gene, valuePath); + if (treatment !== undefined) { + return collectUUIDs({ type: 'treatment', obj: treatment, uuidType: 'evidenceOnly' }); + } else if (tumor !== undefined) { + return collectUUIDs({ type: 'tumor', obj: tumor, uuidType: 'evidenceOnly' }); + } else if (mutation !== undefined) { + return collectUUIDs({ type: 'mutation', obj: mutation, uuidType: 'evidenceOnly' }); + } else { + return undefined; + } +} + +export function pathToGetEvidenceArgs({ + gene, + valuePath, + updateTime, + drugListRef, + entrezGeneId, +}: { + valuePath: string; +} & Pick): GetEvidenceArgs | undefined { + const args: Partial = { + updateTime, + gene, + drugListRef, + entrezGeneId, + ...extractObjsInValuePath(gene, valuePath), + }; + + const tiRegex = /^mutations\/\d+\/tumors\/\d+\/TIs\/\d+\/treatments\/(\d+)(?!\/short$|\/indication$)/; + + let type: GetEvidenceArgs['type'] | undefined = undefined; + + if (valuePath === 'summary') { + type = EvidenceEvidenceTypeEnum.GeneSummary; + } else if (/^mutations\/\d+\/tumors\/\d+\/summary/.test(valuePath)) { + type = EvidenceEvidenceTypeEnum.TumorTypeSummary; + } else if (/^mutations\/\d+\/tumors\/\d+\/prognosticSummary/.test(valuePath)) { + type = EvidenceEvidenceTypeEnum.PrognosticSummary; + } else if (/^mutations\/\d+\/tumors\/\d+\/diagnosticSummary/.test(valuePath)) { + type = EvidenceEvidenceTypeEnum.DiagnosticSummary; + } else if (valuePath === 'background') { + type = EvidenceEvidenceTypeEnum.GeneBackground; + } else if (/^mutations\/\d+\/mutation_effect\/(effect|description)/.test(valuePath)) { + type = EvidenceEvidenceTypeEnum.MutationEffect; + } else if (/^mutations\/\d+\/tumors\/\d+\/prognostic\/.*/.test(valuePath)) { + type = EvidenceEvidenceTypeEnum.PrognosticImplication; + } else if (/^mutations\/\d+\/tumors\/\d+\/diagnostic\/.*/.test(valuePath)) { + type = EvidenceEvidenceTypeEnum.DiagnosticImplication; + } else if (/^mutations\/\d+\/mutation_effect\/oncogenic/.test(valuePath)) { + type = EvidenceEvidenceTypeEnum.Oncogenic; + } else if (/^mutations\/\d+\/summary/.test(valuePath)) { + type = EvidenceEvidenceTypeEnum.MutationSummary; + } else if (/^mutations\/\d+\/name/.test(valuePath)) { + type = 'MUTATION_NAME_CHANGE'; + } else if (/^mutations\/\d+\/tumors\/\d+\/(cancerTypes|excludedCancerTypes)/.test(valuePath)) { + type = 'TUMOR_NAME_CHANGE'; + } else if (/^mutations\/\d+\/tumors\/\d+\/TIs\/\d+\/treatments\/\d+\/name$/.test(valuePath)) { + type = 'TREATMENT_NAME_CHANGE'; + } else if (tiRegex.test(valuePath)) { + const treatmentIndex = +(tiRegex.exec(valuePath)?.[1] ?? 0); + const level = args.ti?.treatments[treatmentIndex]?.level; + + switch (level) { + case TX_LEVELS.LEVEL_1: + case TX_LEVELS.LEVEL_2: + type = EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugSensitivity; + break; + case TX_LEVELS.LEVEL_3A: + case TX_LEVELS.LEVEL_3B: + case TX_LEVELS.LEVEL_4: + type = EvidenceEvidenceTypeEnum.InvestigationalTherapeuticImplicationsDrugSensitivity; + break; + case TX_LEVELS.LEVEL_R1: + type = EvidenceEvidenceTypeEnum.StandardTherapeuticImplicationsForDrugResistance; + break; + case TX_LEVELS.LEVEL_R2: + type = EvidenceEvidenceTypeEnum.InvestigationalTherapeuticImplicationsDrugResistance; + break; + default: + return undefined; + } + } else { + // skipping + // TumorTypeSummary: 'TUMOR_TYPE_SUMMARY', + // Vus: 'VUS', + return undefined; + } + + args.type = type; + + return args as GetEvidenceArgs; +} + +function extractObjsInValuePath(gene: Gene, valuePath: string) { + const args: Pick, 'mutation' | 'tumor' | 'ti' | 'treatment'> = {}; + let curObj: unknown = gene; + let previousKey = ''; + for (const key of valuePath.split('/')) { + const index = parseInt(key, 10); + let propertyName = key; + if (_.isObject(curObj) && key in curObj) { + curObj = curObj[key]; + } else if (_.isArray(curObj)) { + propertyName = previousKey; + curObj = curObj[index]; + if (curObj === undefined) { + throw new Error(`Could not find index "${key}" for the array "${previousKey}" in the value path "${valuePath}"`); + } + } else { + throw new Error(`Could not find property "${key}" in the value path "${valuePath}"`); + } + + if (_.isObject(curObj)) { + if ('mutation_effect' in curObj) { + args.mutation = curObj as Mutation; + } else if ('cancerTypes' in curObj) { + args.tumor = curObj as Tumor; + } else if ('treatments' in curObj) { + args.ti = curObj as TI; + } else if ('indication' in curObj) { + args.treatment = curObj as Treatment; + } + } + + previousKey = key; + } + return args; +} + +export function getEvidence({ + type, + mutation, + tumor, + ti, + treatment, + gene, + drugListRef, + updateTime, + entrezGeneId, +}: GetEvidenceArgs): Record { + const evidenceData = resolveTypeSpecificData({ type, entrezGeneId, gene, mutation, tumor, treatment, updateTime }); + + if (ti && treatment) { + handleTi({ treatment, evidenceData, ti, drugListRef, updateTime }); + } + + const evidences: Record = {}; + + if (mutation && 'TUMOR_NAME_CHANGE' !== type && 'TREATMENT_NAME_CHANGE' !== type) { + if (evidenceData.dataUUID || type === 'MUTATION_NAME_CHANGE') { + const alterations = mutation.name + .split(',') + .map(x => x.trim()) + .filter(x => !x) + .map(part => { + return { + alteration: part, + gene: { + hugoSymbol: gene.name, + }, + }; + }); + + if (alterations.length > 0) { + evidenceData.data.alterations = alterations; + } + } + } + + if (type === 'TREATMENT_NAME_CHANGE' || type === 'MUTATION_NAME_CHANGE' || type === 'TUMOR_NAME_CHANGE') { + let uuids: string[]; + if (type === 'MUTATION_NAME_CHANGE') { + uuids = collectUUIDs({ type: 'mutation', obj: mutation, uuidType: 'evidenceOnly' }); + } else if (type === 'TUMOR_NAME_CHANGE') { + uuids = collectUUIDs({ type: 'tumor', obj: tumor, uuidType: 'evidenceOnly' }); + } else { + uuids = collectUUIDs({ type: 'treatment', obj: treatment, uuidType: 'evidenceOnly' }); + } + + for (const uuid of uuids) { + evidences[uuid] = evidenceData.data; + } + } else if (evidenceData.dataUUID) { + evidences[evidenceData.dataUUID] = evidenceData.data; + } + + return evidences; +} + +function handleTi({ + evidenceData, + treatment, + ti, + drugListRef, + updateTime, +}: Pick & { + evidenceData: ReturnType; +}) { + evidenceData.dataUUID = treatment.name_uuid; + evidenceData.data.lastEdit = validateTimeFormat(updateTime); + evidenceData.data.levelOfEvidence = LEVEL_MAPPING[treatment.level]; + evidenceData.data.description = treatment.description; + evidenceData.data.solidPropagationLevel = LEVEL_MAPPING[treatment.propagation]; + evidenceData.data.liquidPropagationLevel = LEVEL_MAPPING[treatment.propagationLiquid]; + evidenceData.data.fdaLevel = FDA_LEVEL_MAPPING[treatment.fdaLevel]; + evidenceData.data.treatments = []; + const treatments = treatment.name.split(','); + const priorities = getNewPriorities(ti.treatments, [evidenceData.dataUUID]); + + for (let i = 0; i < treatments.length; i++) { + const drugs = treatments[i].split('+'); + const drugList: Drug[] = []; + for (let j = 0; j < drugs.length; j++) { + const drugName = drugs[j].trim(); + const drugObj = drugListRef[drugName]; + if (drugObj) { + drugObj.priority = j + 1; + drugList.push(drugObj); + } else { + throw new Error('Drug is not available.' + drugName); + } + } + evidenceData.data.treatments.push({ + approvedIndications: treatment.indication ? treatment.indication[treatment.indication] : [], + drugs: drugList, + priority: priorities[evidenceData.dataUUID][drugList.map(x => x.uuid).join(' + ')], + }); + } +} diff --git a/src/main/webapp/app/shared/util/core-evidence-submission/type-specific-resolvers.ts b/src/main/webapp/app/shared/util/core-evidence-submission/type-specific-resolvers.ts new file mode 100644 index 000000000..940e72a5d --- /dev/null +++ b/src/main/webapp/app/shared/util/core-evidence-submission/type-specific-resolvers.ts @@ -0,0 +1,301 @@ +import _ from 'lodash'; +import { GetEvidenceArgs, KnownEffect } from './core-evidence-submission'; +import { mostRecentItem, LEVEL_MAPPING, validateTimeFormat, collectUUIDs } from './core-evidence-submission-utils'; +import { Alteration, Evidence, TumorType } from '../../api/generated/core/api'; +import { Mutation } from 'app/shared/model/firebase/firebase.model'; + +function handleInvestigationalResistanceToTherapy({ evidenceData }: { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.knownEffect = 'Resistant'; +} + +function handleInvestigationalSensitivityToTherapy({ evidenceData }: { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.knownEffect = 'Sensitive'; +} + +function handleStandardResistanceToTherapy({ evidenceData }: { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.knownEffect = 'Resistant'; +} + +function handleStandardSensitivityToTherapy({ evidenceData }: { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.knownEffect = 'Sensitive'; +} + +function handleTherapyExcludedRCTs({ + evidenceData, + treatment, +}: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + if (treatment.excludedRCTs) { + evidenceData.data.excludedCancerTypes = treatment.excludedRCTs as TumorType[]; + } +} + +function handlePrognosticSummary({ tumor, evidenceData }: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.description = tumor.prognosticSummary; + evidenceData.dataUUID = tumor.prognosticSummary_uuid; + evidenceData.data.lastEdit = validateTimeFormat(tumor.prognosticSummary_review?.updateTime); +} + +function handleDiagnosticSummary({ tumor, evidenceData }: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.description = tumor.diagnosticSummary; + evidenceData.dataUUID = tumor.diagnosticSummary_uuid; + evidenceData.data.lastEdit = validateTimeFormat(tumor.diagnosticSummary_review?.updateTime); +} + +function handleTumorTypeSummary({ tumor, evidenceData }: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.description = tumor.summary; + evidenceData.dataUUID = tumor.summary_uuid; + evidenceData.data.lastEdit = validateTimeFormat(tumor.summary_review?.updateTime); +} + +function handleOncogenic({ mutation, evidenceData }: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + const MEObj = mutation.mutation_effect; + evidenceData.data.evidenceType = 'ONCOGENIC'; + evidenceData.data.knownEffect = MEObj.oncogenic; + evidenceData.dataUUID = MEObj.oncogenic_uuid; + evidenceData.data.lastEdit = validateTimeFormat(MEObj.oncogenic_review?.updateTime); +} + +function handleMutationEffect({ mutation, evidenceData }: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + const MEObj = mutation.mutation_effect; + const tempReviewObjArr = [MEObj.effect_review, MEObj.description_review]; + const tempRecentIndex = mostRecentItem(tempReviewObjArr, true); + evidenceData.data.knownEffect = MEObj.effect as KnownEffect; + evidenceData.dataUUID = MEObj.effect_uuid; + evidenceData.data.lastEdit = validateTimeFormat(tempReviewObjArr[tempRecentIndex]?.updateTime); + evidenceData.data.description = MEObj.description; + evidenceData.data.evidenceType = 'MUTATION_EFFECT'; +} + +function handleDiagnosticImplication({ + tumor, + updateTime, + evidenceData, +}: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.description = tumor.diagnostic.description; + evidenceData.data.levelOfEvidence = LEVEL_MAPPING[tumor.diagnostic.level]; + if (tumor.diagnostic.excludedRCTs) { + evidenceData.data.excludedCancerTypes = tumor.prognostic.excludedRCTs as TumorType[]; + } + evidenceData.dataUUID = tumor.diagnostic_uuid; + evidenceData.data.lastEdit = validateTimeFormat(updateTime); +} + +function handlePrognosticImplication({ + tumor, + updateTime, + evidenceData, +}: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.description = tumor.prognostic.description; + evidenceData.data.levelOfEvidence = LEVEL_MAPPING[tumor.prognostic.level]; + if (tumor.prognostic.excludedRCTs) { + evidenceData.data.excludedCancerTypes = tumor.prognostic.excludedRCTs as TumorType[]; + } + evidenceData.dataUUID = tumor.prognostic_uuid; + evidenceData.data.lastEdit = validateTimeFormat(updateTime); +} + +function handleGeneBackground({ gene, evidenceData }: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.description = gene.background; + evidenceData.dataUUID = gene.background_uuid; + evidenceData.data.lastEdit = validateTimeFormat(gene.background_review?.updateTime); +} + +function handleGeneSummary({ gene, evidenceData }: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.description = gene.summary; + evidenceData.dataUUID = gene.summary_uuid; + evidenceData.data.lastEdit = validateTimeFormat(gene.summary_review?.updateTime); +} + +function handleMutationNameChange({ + evidenceData, + gene, + mutation, +}: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.alterations = getAlterations(gene.name, mutation); +} + +function handleMutationSummary({ + evidenceData, + mutation, +}: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.evidenceType = 'MUTATION_SUMMARY'; + evidenceData.dataUUID = mutation.summary_uuid; + evidenceData.data.description = mutation.summary; +} + +function handleTumorNameChange({ evidenceData, tumor }: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.cancerTypes = tumor.cancerTypes as TumorType[]; + evidenceData.data.excludedCancerTypes = tumor.excludedCancerTypes as TumorType[]; +} + +function handleTreatmentNameChange({ + evidenceData, + treatment, +}: Pick & { evidenceData: InitializeEvidenceDataRtn }) { + evidenceData.data.excludedCancerTypes = treatment.excludedRCTs as TumorType[]; +} + +type InitializeEvidenceDataRtn = { + data: Evidence; + dataUUID: string; +}; + +function initializeEvidenceData({ + type, + gene, + entrezGeneId, +}: Pick): InitializeEvidenceDataRtn { + const data: Evidence = { + additionalInfo: null, + alterations: null, + description: null, + evidenceType: type === 'TUMOR_NAME_CHANGE' || type === 'MUTATION_NAME_CHANGE' || type === 'TREATMENT_NAME_CHANGE' ? null : type, + gene: { + hugoSymbol: gene.name, + }, + knownEffect: null, + lastEdit: null, + levelOfEvidence: null, + articles: null, + treatments: null, + solidPropagationLevel: null, + liquidPropagationLevel: null, + fdaLevel: null, + cancerTypes: null, + excludedCancerTypes: null, + relevantCancerTypes: null, + } as unknown as Evidence; + + if (data.gene !== undefined) { + data.gene.entrezGeneId = entrezGeneId; + } + + return { + data, + dataUUID: '', + }; +} + +export function resolveTypeSpecificData({ + gene, + type, + tumor, + mutation, + updateTime, + entrezGeneId, + treatment, +}: Pick) { + const evidenceData = initializeEvidenceData({ type, gene, entrezGeneId }); + switch (type) { + case 'GENE_SUMMARY': + handleGeneSummary({ evidenceData, gene }); + break; + case 'GENE_BACKGROUND': + handleGeneBackground({ evidenceData, gene }); + break; + case 'MUTATION_EFFECT': + handleMutationEffect({ evidenceData, mutation }); + break; + case 'TUMOR_TYPE_SUMMARY': + handleTumorTypeSummary({ evidenceData, tumor }); + break; + case 'DIAGNOSTIC_SUMMARY': + handleDiagnosticSummary({ evidenceData, tumor }); + break; + case 'PROGNOSTIC_SUMMARY': + handlePrognosticSummary({ evidenceData, tumor }); + break; + case 'PROGNOSTIC_IMPLICATION': + handlePrognosticImplication({ evidenceData, tumor, updateTime }); + break; + case 'DIAGNOSTIC_IMPLICATION': + handleDiagnosticImplication({ evidenceData, tumor, updateTime }); + break; + case 'STANDARD_THERAPEUTIC_IMPLICATIONS_FOR_DRUG_SENSITIVITY': + handleStandardSensitivityToTherapy({ evidenceData }); + handleTherapyExcludedRCTs({ evidenceData, treatment }); + break; + case 'STANDARD_THERAPEUTIC_IMPLICATIONS_FOR_DRUG_RESISTANCE': + handleStandardResistanceToTherapy({ evidenceData }); + handleTherapyExcludedRCTs({ evidenceData, treatment }); + break; + case 'INVESTIGATIONAL_THERAPEUTIC_IMPLICATIONS_DRUG_SENSITIVITY': + handleInvestigationalSensitivityToTherapy({ evidenceData }); + handleTherapyExcludedRCTs({ evidenceData, treatment }); + break; + case 'INVESTIGATIONAL_THERAPEUTIC_IMPLICATIONS_DRUG_RESISTANCE': + handleInvestigationalResistanceToTherapy({ evidenceData }); + handleTherapyExcludedRCTs({ evidenceData, treatment }); + break; + case 'ONCOGENIC': + handleOncogenic({ mutation, evidenceData }); + break; + case 'MUTATION_NAME_CHANGE': + handleMutationNameChange({ evidenceData, gene, mutation }); + break; + case 'MUTATION_SUMMARY': + handleMutationSummary({ evidenceData, mutation }); + break; + case 'TUMOR_NAME_CHANGE': + handleTumorNameChange({ evidenceData, tumor }); + break; + case 'TREATMENT_NAME_CHANGE': + handleTreatmentNameChange({ evidenceData, treatment }); + break; + default: + throw new Error(`Unknown evidence type "${type}"`); + } + + if (evidenceData.data.evidenceType) { + // attach alterations when available, so we can create new evidence if it does not exist in server side + if (mutation) { + if (!evidenceData.data.alterations) { + evidenceData.data.alterations = getAlterations(gene.name, mutation); + } + } + if (tumor) { + if (!evidenceData.data.cancerTypes) { + evidenceData.data.cancerTypes = tumor.cancerTypes as TumorType[]; + } + if (!evidenceData.data.excludedCancerTypes) { + evidenceData.data.excludedCancerTypes = tumor.excludedCancerTypes as TumorType[]; + } + } + } + + if (!evidenceData.data.cancerTypes) { + evidenceData.data.cancerTypes = []; + } + if (!evidenceData.data.relevantCancerTypes) { + evidenceData.data.relevantCancerTypes = []; + } + if (!evidenceData.data.excludedCancerTypes) { + evidenceData.data.excludedCancerTypes = []; + } + + return evidenceData; +} + +function getAlterations(geneName: string, mutation: Mutation) { + const alterations = mutation.alterations; + const altResults: Alteration[] = []; + if (mutation.name && (alterations === undefined || alterations.length === 0)) { + altResults.push({ + alteration: mutation.name.trim(), + gene: { + hugoSymbol: geneName, + }, + }); + } else if (alterations !== undefined && alterations.length > 0) { + for (const alteration of alterations) { + altResults.push({ + alteration: alteration.name, + gene: { + hugoSymbol: geneName, + }, + }); + } + } + + return altResults; +} diff --git a/src/main/webapp/app/shared/util/core-gene-type-submission/core-gene-type-submission.spec.ts b/src/main/webapp/app/shared/util/core-gene-type-submission/core-gene-type-submission.spec.ts new file mode 100644 index 000000000..bc066810e --- /dev/null +++ b/src/main/webapp/app/shared/util/core-gene-type-submission/core-gene-type-submission.spec.ts @@ -0,0 +1,86 @@ +import 'jest-expect-message'; +import { createGeneTypePayload } from './core-gene-type-submission'; +import { GENE_TYPE } from 'app/config/constants/firebase'; + +describe('getGeneType', () => { + type CreateGeneTypePayloadParameters = Parameters; + const tests: [CreateGeneTypePayloadParameters[0], Required>][] = [ + [ + { name: 'name', type: { ocg: GENE_TYPE.ONCOGENE, tsg: GENE_TYPE.TUMOR_SUPPRESSOR } }, + { + hugoSymbol: 'name', + tsg: true, + oncogene: true, + }, + ], + [ + { + name: 'name', + type: { + ocg: GENE_TYPE.ONCOGENE, + ocg_review: { updatedBy: '', updateTime: 0 }, + tsg: GENE_TYPE.TUMOR_SUPPRESSOR, + tsg_review: { updatedBy: '', updateTime: 0 }, + }, + }, + { + hugoSymbol: 'name', + tsg: true, + oncogene: true, + }, + ], + [ + { + name: 'name', + type: { ocg: GENE_TYPE.ONCOGENE, tsg: '', tsg_review: { updatedBy: '', updateTime: 0 } }, + }, + { + hugoSymbol: 'name', + tsg: false, + oncogene: true, + }, + ], + [ + { + name: 'name', + type: { ocg: GENE_TYPE.ONCOGENE, tsg: '', tsg_review: { updatedBy: '', updateTime: 0 } }, + }, + { + hugoSymbol: 'name', + tsg: false, + oncogene: true, + }, + ], + [ + { + name: 'name', + type: { ocg: '', ocg_review: { updatedBy: '', updateTime: 0 }, tsg: GENE_TYPE.TUMOR_SUPPRESSOR }, + }, + { + hugoSymbol: 'name', + tsg: true, + oncogene: false, + }, + ], + [ + { name: 'name', type: { ocg: '', tsg: GENE_TYPE.TUMOR_SUPPRESSOR } }, + { + hugoSymbol: 'name', + tsg: true, + oncogene: false, + }, + ], + [ + { name: 'name', type: { ocg: '', tsg: '' } }, + { + hugoSymbol: 'name', + tsg: false, + oncogene: false, + }, + ], + ]; + + test.each(tests)('Should map to payload correctly %j with value path "%s"', (gene, expected) => { + expect(createGeneTypePayload(gene)).toEqual(expected); + }); +}); diff --git a/src/main/webapp/app/shared/util/core-gene-type-submission/core-gene-type-submission.ts b/src/main/webapp/app/shared/util/core-gene-type-submission/core-gene-type-submission.ts new file mode 100644 index 000000000..467a6717f --- /dev/null +++ b/src/main/webapp/app/shared/util/core-gene-type-submission/core-gene-type-submission.ts @@ -0,0 +1,22 @@ +import { Gene as GenePayload } from 'app/shared/api/generated/core'; +import { Gene } from 'app/shared/model/firebase/firebase.model'; + +export function createGeneTypePayload( + gene: Pick & { type: Pick }, +): Required> { + return { + hugoSymbol: gene.name, + oncogene: gene.type.ocg ? true : false, + tsg: gene.type.tsg ? true : false, + }; +} + +export function removeGenePayload(gene: Pick): Required> { + return { + hugoSymbol: gene.name, + }; +} + +export function isGeneTypeChange(path: string): boolean { + return path.startsWith('type'); +} diff --git a/src/main/webapp/app/shared/util/core-submission-shared/core-submission-utils.spec.ts b/src/main/webapp/app/shared/util/core-submission-shared/core-submission-utils.spec.ts new file mode 100644 index 000000000..bf1035125 --- /dev/null +++ b/src/main/webapp/app/shared/util/core-submission-shared/core-submission-utils.spec.ts @@ -0,0 +1,346 @@ +import 'jest-expect-message'; +import { flattenReviewPaths, useLastReviewedOnly } from './core-submission-utils'; +import { Gene } from 'app/shared/model/firebase/firebase.model'; +import { GENE_TYPE } from 'app/config/constants/firebase'; +import _ from 'lodash'; +import { BaseReviewLevel, ReviewLevel } from '../firebase/firebase-review-utils'; + +type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + +describe('useLastReviewedOnly', () => { + const tests: [RecursivePartial, RecursivePartial | undefined][] = [ + [ + { + summary: 'XXXXXXXXXXX', + summary_review: { + lastReviewed: 'YYYYYYYYYYY', + added: true, + updateTime: 0, + updatedBy: 'Test User', + }, + }, + undefined, + ], + [ + { + summary: 'XXXXXXXXXXX', + mutations: [ + { + name_uuid: '37a65ce9-1165-487a-be98-6975805d7e0f', + name_review: { + added: true, + }, + name: 'created mutation', + }, + ], + }, + { + summary: 'XXXXXXXXXXX', + mutations: [], + }, + ], + [ + { + summary: 'XXXXXXXXXXX', + mutations: [ + { + name: 'created cancer type test', + tumors: [ + { + cancerTypes: [ + { + code: 'b2f013d0-608c-43a1-abdf-cba892b1f523', + }, + ], + cancerTypes_review: { added: true }, + }, + ], + }, + ], + }, + { + summary: 'XXXXXXXXXXX', + mutations: [ + { + name: 'created cancer type test', + tumors: [], + }, + ], + }, + ], + [ + { + summary: 'XXXXXXXXXXX', + mutations: [ + { + tumors: [ + { + TIs: [ + { + name: 'created treatment test', + treatments: [ + { + name: 'test', + name_review: { added: true }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + summary: 'XXXXXXXXXXX', + mutations: [ + { + tumors: [ + { + TIs: [ + { + name: 'created treatment test', + treatments: [], + }, + ], + }, + ], + }, + ], + }, + ], + [ + { + summary: 'XXXXXXXXXXX', + summary_review: { + lastReviewed: 'YYYYYYYYYYY', + updateTime: 0, + updatedBy: 'Test User', + }, + }, + { + summary: 'YYYYYYYYYYY', + summary_review: { + updateTime: 0, + updatedBy: 'Test User', + }, + }, + ], + [ + { + type: { + ocg: GENE_TYPE.ONCOGENE, + ocg_review: { + lastReviewed: '', + updateTime: 0, + updatedBy: 'Test User', + }, + }, + }, + { + type: { + ocg: '', + ocg_review: { + updateTime: 0, + updatedBy: 'Test User', + }, + }, + }, + ], + [ + { + mutations: [ + { + name: 'mutation name', + name_review: { + updateTime: 0, + updatedBy: 'Test User', + }, + }, + { + name: 'mutation name', + name_review: { + lastReviewed: 'new mutation name', + updateTime: 0, + updatedBy: 'Test User', + }, + }, + { + name: 'mutation name2', + }, + ], + }, + { + mutations: [ + { + name: 'mutation name', + name_review: { + updateTime: 0, + updatedBy: 'Test User', + }, + }, + { + name: 'new mutation name', + name_review: { + updateTime: 0, + updatedBy: 'Test User', + }, + }, + { + name: 'mutation name2', + }, + ], + }, + ], + ]; + + test.each(tests)('Should only use last reviewed value %j', (obj, expected) => { + const copy = _.cloneDeep(obj); + expect(useLastReviewedOnly(obj)).toEqual(expected); + expect(copy, 'The passed object should be untouched').toEqual(obj); + }); +}); + +describe('findAllChildReviewPaths', () => { + const tests: [ + RecursivePartial[], + RecursivePartial[], + ][] = [ + [ + [ + { + valuePath: 'mutations/12/name', + children: [ + { + valuePath: 'mutations/12/name/mutation_effect', + children: [ + { + valuePath: 'mutations/12/mutation_effect/description', + children: [], + reviewInfo: { + reviewPath: 'mutations/12/mutation_effect/description_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/12/mutation_effect/effect', + children: [], + reviewInfo: { + reviewPath: 'mutations/12/mutation_effect/effect_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/12/mutation_effect/oncogenic', + children: [], + reviewInfo: { + reviewPath: 'mutations/12/mutation_effect/oncogenic_review', + reviewAction: 2, + }, + }, + ], + }, + ], + reviewInfo: { + reviewPath: 'mutations/12/name_review', + reviewAction: 0, + }, + }, + { + valuePath: 'mutations/13/name', + children: [ + { + children: [ + { + valuePath: 'mutations/13/mutation_effect/description', + children: [], + reviewInfo: { + reviewPath: 'mutations/13/mutation_effect/description_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/13/mutation_effect/effect', + children: [], + reviewInfo: { + reviewPath: 'mutations/13/mutation_effect/effect_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/13/mutation_effect/oncogenic', + children: [], + reviewInfo: { + reviewPath: 'mutations/13/mutation_effect/oncogenic_review', + reviewAction: 2, + }, + }, + ], + }, + ], + reviewInfo: { + reviewPath: 'mutations/13/name_review', + reviewAction: 0, + }, + }, + ], + [ + { + valuePath: 'mutations/12/mutation_effect/description', + children: [], + reviewInfo: { + reviewPath: 'mutations/12/mutation_effect/description_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/12/mutation_effect/effect', + children: [], + reviewInfo: { + reviewPath: 'mutations/12/mutation_effect/effect_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/12/mutation_effect/oncogenic', + children: [], + reviewInfo: { + reviewPath: 'mutations/12/mutation_effect/oncogenic_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/13/mutation_effect/description', + children: [], + reviewInfo: { + reviewPath: 'mutations/13/mutation_effect/description_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/13/mutation_effect/effect', + children: [], + reviewInfo: { + reviewPath: 'mutations/13/mutation_effect/effect_review', + reviewAction: 2, + }, + }, + { + valuePath: 'mutations/13/mutation_effect/oncogenic', + children: [], + reviewInfo: { + reviewPath: 'mutations/13/mutation_effect/oncogenic_review', + reviewAction: 2, + }, + }, + ], + ], + ]; + + test.each(tests)('Should find all child review paths in %j for path "%s"', (reviews, expected) => { + const actual = reviews.flatMap(x => flattenReviewPaths(x as BaseReviewLevel)); + expect(actual).toEqual(expected); + }); +}); diff --git a/src/main/webapp/app/shared/util/core-submission-shared/core-submission-utils.ts b/src/main/webapp/app/shared/util/core-submission-shared/core-submission-utils.ts new file mode 100644 index 000000000..a2c7c8918 --- /dev/null +++ b/src/main/webapp/app/shared/util/core-submission-shared/core-submission-utils.ts @@ -0,0 +1,121 @@ +import { Review } from 'app/shared/model/firebase/firebase.model'; +import { BaseReviewLevel, ReviewLevel } from '../firebase/firebase-review-utils'; + +/** + * Determines whether the `path` should be protected based on the review path. + * + * Protection is based on whether the current path or parent path aligns with the `reviewPath`, + * meaning that the data was just accepted by a user. + * + * @param [reviewPaths] - The reference path for review, indicating an accepted field. Undefined when nothing was accepted. + * @param path - The path of the current element being evaluated. + * @returns - Returns true if the `path` should be protected, false otherwise. + */ +function isPathProtected(reviewPaths: string[], path: string): boolean { + for (const reviewPath of reviewPaths) { + const shouldProtect = reviewPath !== undefined && path.length > 0 && (path.includes(reviewPath) || reviewPath.includes(path)); + if (shouldProtect) { + return true; + } + } + return false; +} + +/** + * Recursively filters the provided object to include only the last reviewed elements + * or elements that where just accepted. + * + * Paths are protected if they match the review path or are part of its hierarchy. + * Protected paths retain their original values, while non-protected paths + * only use their`lastReviewed` value. + * + * @param obj - The object to be filtered for last reviewed elements. + * @param [reviewPaths] - The paths used to determine if an element should be protected. + * @returns - The filtered object or undefined if no elements are accepted. + */ +export function useLastReviewedOnly(obj: T, ...reviewPaths: string[]): T | undefined { + function processObjectRecursively(currentObj: T | null | undefined, parentPath: string): T | null | undefined { + if (currentObj === null || currentObj === undefined) { + return currentObj; + } + const resultObj: T = {} as T; + + for (const [key, value] of Object.entries(currentObj as Record)) { + const currentPath = `${parentPath}${parentPath.length > 0 ? '/' : ''}${key}`; + const isCurrentPathProtected = isPathProtected(reviewPaths, currentPath); + const isParentPathProtected = isPathProtected(reviewPaths, parentPath); + + // If the key ends with "_review", copy it to the new object + if (key.endsWith('_review')) { + const reviewData: Review = Object.assign({} as Review, value); + if ('added' in reviewData) { + delete reviewData.added; + } + if ('lastReviewed' in reviewData) { + delete reviewData.lastReviewed; + } + (resultObj as Record)[key] = reviewData; + } else if (Array.isArray(value)) { + const reviewData: Review | undefined = currentObj[`${key}_review`] as Review; + // If the object is new (indicated by `added`) and the parent path is not protected, exclude it from the result. + if (reviewData?.added && !isParentPathProtected) { + return undefined; + } + const processedArray: (T | null | undefined)[] = []; + let i = 0; + for (const arrayElement of value) { + const processedElement = processObjectRecursively(arrayElement, `${currentPath}/${i}`); + // Preserve undefined values in the array to maintain the correct index structure, + // ensuring paths like "mutation/11/..." are still valid even if some elements are omitted. + // If the parent path is protected, push the element regardless of whether it's undefined. + if (processedElement !== undefined || isCurrentPathProtected) { + processedArray.push(processedElement); + } + i++; + } + (resultObj as Record)[key] = processedArray; + } else if (typeof value === 'object') { + (resultObj as Record)[key] = processObjectRecursively(value as T, currentPath); + } else { + const reviewData: Review | undefined = currentObj[`${key}_review`] as Review; + + if (isCurrentPathProtected) { + // If the current path is protected, it indicates the current field was accepted, + // so we retain its original value. + (resultObj as Record)[key] = value; + } else if (!reviewData?.added || isParentPathProtected) { + // If the field is not new (i.e., `added` is false) or the parent path is protected, + // we use the last reviewed value (if available) or fall back to the original value. + (resultObj as Record)[key] = reviewData?.lastReviewed !== undefined ? reviewData.lastReviewed : value; + } else { + // If the object is new (added) and neither the current path nor parent path is protected, + // exclude the whole resultObj from the result. + return undefined; + } + } + } + return resultObj; + } + + return processObjectRecursively(obj, '') as T | undefined; +} + +export function flattenReviewPaths(reviewLevel: BaseReviewLevel): ReviewLevel[] { + const arr: ReviewLevel[] = []; + + if (reviewLevel.children !== undefined && reviewLevel.children.length > 0) { + for (const childLevel of reviewLevel.children) { + arr.push(...flattenReviewPaths(childLevel)); + } + } else if ( + 'reviewInfo' in reviewLevel && + typeof reviewLevel.reviewInfo === 'object' && + reviewLevel.reviewInfo !== null && + 'reviewAction' in reviewLevel.reviewInfo && + reviewLevel.reviewInfo.reviewAction !== undefined + ) { + arr.push(reviewLevel as ReviewLevel); + } + + return arr; +} diff --git a/src/main/webapp/app/shared/util/core-submission-shared/core-submission.mocks.ts b/src/main/webapp/app/shared/util/core-submission-shared/core-submission.mocks.ts new file mode 100644 index 000000000..474bbfab0 --- /dev/null +++ b/src/main/webapp/app/shared/util/core-submission-shared/core-submission.mocks.ts @@ -0,0 +1,133 @@ +import { + Drug, + Gene, + GeneType, + Implication, + Mutation, + MutationEffect, + Review, + TI, + TI_TYPE, + Treatment, + Tumor, + CancerType, + CancerRisk, + MutationSpecificInheritanceMechanism, + MutationSpecificPenetrance, + Vus, + VusTime, + Comment, + VusTimeBy, +} from '../../model/firebase/firebase.model'; + +export function createMockGene(gene: Partial = {}): Gene { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Gene('name'), gene); +} + +export function createMockTreatment(treatment: Partial = {}): Treatment { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Treatment('a'), treatment); +} + +export function createMockReview(review: Partial = {}): Review { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Review('SYSTEM'), review); +} + +export function createMockTumor(tumor: Partial = {}): Tumor { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Tumor(), tumor); +} + +export function createMockImplication(implication: Partial = {}): Implication { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Implication(), implication); +} + +export function createMockMutationEffect(mutationEffect: Partial = {}): MutationEffect { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new MutationEffect(), mutationEffect); +} + +export function createMockMutation(mutation: Partial = {}): Mutation { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Mutation('mutation'), mutation); +} + +export function createMockDrug(drug: Partial) { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Drug('drug', 'ncitCode', 'ncitName', 1, []), drug); +} + +export function createMockTi(ti: Partial = {}): TI { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new TI(TI_TYPE.IR), ti); +} + +export function createMockGeneType(geneType: Partial = {}): GeneType { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new GeneType(), geneType); +} + +export function createMockCancerType(cancerType: Partial = {}): CancerType { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new CancerType(), cancerType); +} + +export function createMockCancerRisk(cancerRisk: Partial = {}): CancerRisk { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new CancerRisk(), cancerRisk); +} + +export function createMockMutationSpecificPenetrance( + mutationSpecificPenetrance: Partial = {}, +): MutationSpecificPenetrance { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new MutationSpecificPenetrance(), mutationSpecificPenetrance); +} + +export function createMockMutationSpecificInheritanceMechanism( + mutationSpecificInheritanceMechanism: Partial = {}, +): MutationSpecificInheritanceMechanism { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new MutationSpecificInheritanceMechanism(), mutationSpecificInheritanceMechanism); +} + +export function createMockVus(vus: Partial = {}): Vus { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Vus('', '', ''), vus); +} + +export function createMockVusTime(vusTime: Partial = {}): VusTime { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new VusTime('', ''), vusTime); +} + +export function createMockComment(comment: Partial = {}): Comment { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new Comment(), comment); +} + +export function createMockVusBy(vusTimeBy: Partial = {}): VusTimeBy { + // coming from firebase the class is not used to create the object so + // instanceof cannot be used which is why empty object is being used + return Object.assign({}, new VusTimeBy('', ''), vusTimeBy); +} diff --git a/src/main/webapp/app/shared/util/entity-utils/gene-entity-utils.ts b/src/main/webapp/app/shared/util/entity-utils/gene-entity-utils.ts new file mode 100644 index 000000000..43aff8ddd --- /dev/null +++ b/src/main/webapp/app/shared/util/entity-utils/gene-entity-utils.ts @@ -0,0 +1,6 @@ +import { IFlag } from 'app/shared/model/flag.model'; +import { IGene } from 'app/shared/model/gene.model'; + +export function geneIsReleased(gene: IGene): boolean { + return (gene.flags || []).some(flag => flag.type === 'GENE_PANEL' && flag.flag === 'ONCOKB'); +} diff --git a/src/main/webapp/app/shared/util/firebase/firebase-review-utils.tsx b/src/main/webapp/app/shared/util/firebase/firebase-review-utils.tsx index 5b274b0c0..7d09099c5 100644 --- a/src/main/webapp/app/shared/util/firebase/firebase-review-utils.tsx +++ b/src/main/webapp/app/shared/util/firebase/firebase-review-utils.tsx @@ -401,7 +401,7 @@ export const isIgnoredKey = (key: string) => { }; export const findReviewRecursive = ( - currObj: any, + currObj: Gene | GenomicIndicator | Mutation | Tumor | Treatment, currValuePath: string, uuids: string[], parentReview: BaseReviewLevel, diff --git a/src/main/webapp/app/shared/util/firebase/firebase-utils.spec.ts b/src/main/webapp/app/shared/util/firebase/firebase-utils.spec.ts index 3eb02c3b1..5b7b775f2 100644 --- a/src/main/webapp/app/shared/util/firebase/firebase-utils.spec.ts +++ b/src/main/webapp/app/shared/util/firebase/firebase-utils.spec.ts @@ -33,6 +33,7 @@ import { } from 'app/shared/model/firebase/firebase.model'; import { generateUuid } from '../utils'; import { IDrug } from 'app/shared/model/drug.model'; +import { MUTATION_EFFECT } from 'app/config/constants/constants'; describe('FirebaseUtils', () => { describe('getValueByNestedKey', () => { @@ -609,14 +610,14 @@ describe('FirebaseUtils', () => { it('should get the mutation stats', () => { const mutation = new Mutation(''); mutation.mutation_effect.oncogenic = FIREBASE_ONCOGENICITY.LIKELY; - mutation.mutation_effect.effect = 'effect'; + mutation.mutation_effect.effect = MUTATION_EFFECT.NEUTRAL; mutation.tumors = [tumor1, tumor2]; const expected = { TT: 2, oncogenicity: FIREBASE_ONCOGENICITY.LIKELY, - mutationEffect: 'effect', + mutationEffect: MUTATION_EFFECT.NEUTRAL, TTS: 1, DxS: 1, PxS: 2, diff --git a/src/main/webapp/app/shared/util/firebase/firebase-utils.tsx b/src/main/webapp/app/shared/util/firebase/firebase-utils.tsx index ca6d72144..e75d9ad6f 100644 --- a/src/main/webapp/app/shared/util/firebase/firebase-utils.tsx +++ b/src/main/webapp/app/shared/util/firebase/firebase-utils.tsx @@ -1,5 +1,5 @@ import { APP_EXPANDED_DATETIME_FORMAT, CURRENT_REVIEWER } from 'app/config/constants/constants'; -import { FB_COLLECTION_PATH } from 'app/config/constants/firebase'; +import { FB_COLLECTION, FB_COLLECTION_PATH } from 'app/config/constants/firebase'; import { NestLevelType, RemovableNestLevel } from 'app/pages/curation/collapsible/NestLevel'; import { IDrug } from 'app/shared/model/drug.model'; import { CategoricalAlterationType } from 'app/shared/model/enumerations/categorical-alteration-type.model'; @@ -16,9 +16,9 @@ import { PX_LEVELS, Review, TI, - TX_LEVELS, Treatment, Tumor, + TX_LEVELS, VusObjList, } from 'app/shared/model/firebase/firebase.model'; import _ from 'lodash'; @@ -125,8 +125,13 @@ export const getFirebasePath = (type: keyof typeof FB_COLLECTION_PATH, ...params return replaceUrlParams(FB_COLLECTION_PATH[type], ...params); }; -export const getFirebaseGenePath = (isGermline: boolean | undefined, hugoSymbol: string | undefined) => { - return getFirebasePath(isGermline ? 'GERMLINE_GENE' : 'GENE', hugoSymbol); +export const getFirebaseGenePath = (isGermline: boolean | undefined, hugoSymbol?: string) => { + if (hugoSymbol !== undefined) { + const basePath = isGermline ? 'GERMLINE_GENE' : 'GENE'; + return getFirebasePath(basePath, hugoSymbol); + } else { + return isGermline ? FB_COLLECTION.GERMLINE_GENES : FB_COLLECTION.GENES; + } }; export const getFirebaseMetaGenePath = (isGermline: boolean | undefined, hugoSymbol: string | undefined) => { @@ -137,8 +142,13 @@ export const getFirebaseHistoryPath = (isGermline: boolean | undefined, hugoSymb return getFirebasePath(isGermline ? 'GERMLINE_HISTORY' : 'HISTORY', hugoSymbol); }; -export const getFirebaseVusPath = (isGermline: boolean | undefined, hugoSymbol: string | undefined) => { - return getFirebasePath(isGermline ? 'GERMLINE_VUS' : 'VUS', hugoSymbol); +export const getFirebaseVusPath = (isGermline: boolean | undefined, hugoSymbol?: string) => { + if (hugoSymbol !== undefined) { + const basePath = isGermline ? 'GERMLINE_VUS' : 'VUS'; + return getFirebasePath(basePath, hugoSymbol); + } else { + return isGermline ? FB_COLLECTION.GERMLINE_VUS : FB_COLLECTION.VUS; + } }; export const getFirebaseMetaGeneReviewPath = (isGermline: boolean | undefined, hugoSymbol: string, uuid: string) => { diff --git a/src/main/webapp/app/shared/util/jhipster-types.ts b/src/main/webapp/app/shared/util/jhipster-types.ts index d0d2997ab..eb3d6886e 100644 --- a/src/main/webapp/app/shared/util/jhipster-types.ts +++ b/src/main/webapp/app/shared/util/jhipster-types.ts @@ -15,6 +15,7 @@ export interface IQueryParams { export interface ISearchParams extends IQueryParams { search?: string; exact?: boolean; + noState?: boolean; } export type ICrudGetAllAction = (params: IQueryParams) => IPayload; export type ICrudSearchAction = (params: ISearchParams) => IPayload; diff --git a/src/main/webapp/app/shared/util/pagination-crud-store.ts b/src/main/webapp/app/shared/util/pagination-crud-store.ts index 757c43685..6a7cd38c2 100644 --- a/src/main/webapp/app/shared/util/pagination-crud-store.ts +++ b/src/main/webapp/app/shared/util/pagination-crud-store.ts @@ -50,7 +50,7 @@ export class PaginationCrudStore extends BaseCrudStore { return result; } - *getSearch({ query, exact, page, size, sort }: ISearchParams) { + *getSearch({ query, exact, page, size, sort, noState }: ISearchParams) { let url = `${this.apiUrl}/search?query=${query}${page ? `&page=${page}` : ''}${size ? `&size=${size}` : ''}${ sort ? parseSort(sort) : '' }`; @@ -58,8 +58,10 @@ export class PaginationCrudStore extends BaseCrudStore { url = `${url}&exact=${exact}`; } const result = yield axios.get(url); - this.entities = result.data; - this.totalItems = result.data.length; + if (!noState) { + this.entities = result.data; + this.totalItems = result.data.length; + } return result; } } diff --git a/src/main/webapp/app/shared/util/utils.tsx b/src/main/webapp/app/shared/util/utils.tsx index 7b5d047de..5a9c3c3ae 100644 --- a/src/main/webapp/app/shared/util/utils.tsx +++ b/src/main/webapp/app/shared/util/utils.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { ICancerType } from 'app/shared/model/cancer-type.model'; import { IAlteration } from '../model/alteration.model'; import { v4 as uuidv4 } from 'uuid'; @@ -9,7 +9,7 @@ import EntityActionButton from '../button/EntityActionButton'; import { SORT } from './pagination.constants'; import { PaginationState } from '../table/OncoKBAsyncTable'; import { IUser } from '../model/user.model'; -import { CancerType } from '../model/firebase/firebase.model'; +import { CancerType, DrugCollection } from '../model/firebase/firebase.model'; import _ from 'lodash'; import { ParsedRef, parseReferences } from 'app/oncokb-commons/components/RefComponent'; import { IDrug } from 'app/shared/model/drug.model'; @@ -398,3 +398,24 @@ export const parseSort = (sort: IQueryParams['sort']) => { export const hasValue = (value: T | null | undefined): value is T => { return value !== null && value !== undefined; }; + +export function useDrugListRef(drugList: readonly IDrug[] | undefined = []): DrugCollection { + const [drugListRef, setDrugListRef] = useState({}); + useEffect(() => { + const collection: DrugCollection = drugList.reduce((prev, cur) => { + prev[cur.uuid] = { + uuid: cur.uuid, + drugName: cur.name, + ncitCode: cur.nciThesaurus?.code ?? '', + priority: 0, + description: '', + ncitName: cur.nciThesaurus?.displayName ?? '', + synonyms: cur.nciThesaurus?.synonyms?.map(x => x?.name).filter((x): x is string => x !== undefined) ?? [], + }; + return prev; + }, {} as DrugCollection); + + setDrugListRef(collection); + }, [drugList]); + return drugListRef; +} diff --git a/src/main/webapp/app/stores/createStore.ts b/src/main/webapp/app/stores/createStore.ts index fb74611cf..e1456ba01 100644 --- a/src/main/webapp/app/stores/createStore.ts +++ b/src/main/webapp/app/stores/createStore.ts @@ -101,9 +101,11 @@ import { FirebaseRepository } from './firebase/firebase-repository'; import { OpenMutationCollapsibleStore } from './open-mutation-collapsible.store'; import { CurationPageStore } from 'app/stores/curation-page.store'; import CategoricalAlterationStore from 'app/entities/categorical-alteration/categorical-alteration.store'; +import { driveAnnotationClient, evidenceClient, geneTypeClient, geneLegacyApi } from 'app/shared/api/clients'; import { WindowStore } from './window-store'; /* jhipster-needle-add-store-import - JHipster will add store here */ import ManagementStore from 'app/stores/management.store'; +import { GeneApi } from 'app/shared/api/manual/gene-api'; export interface IRootStore { readonly loadingStore: LoadingBarStore; @@ -165,6 +167,9 @@ export interface IRootStore { readonly firebaseMetaService: FirebaseMetaService; readonly firebaseHistoryService: FirebaseHistoryService; readonly firebaseVusService: FirebaseVusService; + + /* oncokb-core clients */ + readonly geneLegacyApi: GeneApi; } export function createStores(history: History): IRootStore { @@ -239,21 +244,26 @@ export function createStores(history: History): IRootStore { /* Firebase Services */ const firebaseMetaService = new FirebaseMetaService(firebaseRepository, rootStore.authStore); const firebaseHistoryService = new FirebaseHistoryService(firebaseRepository); - const firebaseVusService = new FirebaseVusService(firebaseRepository, rootStore.authStore); + const firebaseVusService = new FirebaseVusService(firebaseRepository, evidenceClient, rootStore.authStore); const firebaseGeneReviewService = new FirebaseGeneReviewService( firebaseRepository, rootStore.authStore, firebaseMetaService, firebaseHistoryService, firebaseVusService, + evidenceClient, + geneTypeClient, ); const firebaseGeneService = new FirebaseGeneService( firebaseRepository, rootStore.authStore, + rootStore.geneStore, + rootStore.drugStore, firebaseMutationListStore, firebaseMutationConvertIconStore, firebaseMetaService, firebaseGeneReviewService, + driveAnnotationClient, ); rootStore.firebaseMetaService = firebaseMetaService; @@ -262,6 +272,9 @@ export function createStores(history: History): IRootStore { rootStore.firebaseGeneService = firebaseGeneService; rootStore.firebaseVusService = firebaseVusService; + /* oncokb-core clients */ + rootStore.geneLegacyApi = geneLegacyApi; + /* jhipster-needle-add-store-init - JHipster will add store here */ return rootStore; } diff --git a/tsconfig.json b/tsconfig.json index cb0981dd4..eff634bc9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "removeComments": false, "suppressImplicitAnyIndexErrors": true, "outDir": "target/classes/static/app", - "lib": ["dom", "es2015", "es2016", "es2017", "es2018"], + "lib": ["dom", "es2015", "es2016", "es2017", "es2018", "ES2021.String"], "types": ["jest", "webpack-env"], "allowJs": true, "checkJs": false,