diff --git a/README.md b/README.md index 4097589a..f6910b0d 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,120 @@ rails s Navigate to `localhost:3000/api/graphiql` in your browser. If the example query provided runs successfully, then you're all set. +### Data loading + +To perform a data load from scratch, first run the `reset` task to provide a clean, seeded DB: + +```shell +rake db:reset +``` + +Most DGIdb data comes from static files, typically called `claims.tsv`. The data loader classes expect `server/lib/data/` to contain the following files: + +``` +lib/data +├── bader_lab +│ └── claims.tsv +├── cancer_commons +│ └── claims.tsv +├── caris_molecular_intelligence +│ └── claims.tsv +├── cgi +│ └── claims.tsv +├── chembl +│ └── chembl.db +├── clearity_foundation_biomarkers +│ └── claims.tsv +├── clearity_foundation_clinical_trial +│ └── claims.tsv +├── cosmic +│ └── claims.csv +├── dgene +│ └── claims.tsv +├── drugbank +│ └── claims.xml +├── dtc +│ └── claims.csv +├── ensembl +│ └── claims.tsv +├── entrez +│ └── claims.tsv +├── fda +│ └── claims.tsv +├── foundation_one_genes +│ └── claims.tsv +├── go +│ └── targets.tsv +├── guide_to_pharmacology +│ ├── interactions.csv +│ └── targets_and_families.csv +├── hingorani_casas +│ └── claims.tsv +├── hopkins_groom +│ └── claims.tsv +├── human_protein_atlas +│ └── claims.tsv +├── idg +│ ├── claims.json +│ └── claims.tsv +├── msk_impact +│ └── claims.tsv +├── my_cancer_genome +│ └── claims.tsv +├── my_cancer_genome_clinical_trial +│ └── claims.tsv +├── nci +│ ├── claims.tsv +│ └── claims.xml +├── oncokb +│ ├── drug_claim.csv +│ ├── gene_claim.csv +│ ├── gene_claim_aliases.csv +│ ├── interaction_claim.csv +│ ├── interaction_claim_attributes.csv +│ └── interaction_claim_links.csv +├── oncomine +│ └── claims.tsv +├── pharmgkb +│ └── claims.tsv +├── russ_lampel +│ └── claims.tsv +├── talc +│ └── claims.tsv +├── tdg_clinical_trial +│ ├── claims.tsv +├── tempus +│ └── claims.tsv +├── tend +│ └── claims.tsv +└── ttd + └── claims.csv +``` + +First, load claims: + +```shell +rake dgidb:import:all +``` + +Then, run grouping. By default, the groupers will expect a normalizer service to be running locally on port 8000; use the `THERAPY_HOSTNAME` and `GENE_HOSTNAME` environment variables to specify alternate hosts: + +```shell +export THERAPY_HOSTNAME=http://localhost:7999 # no trailing backslash +rake dgidb:group:drugs +export GENE_HOSTNAME=http://localhost:7998 # no trailing backslash +rake dgidb:group:genes +rake dgidb:group:interactions +``` + +Finally, normalize remaining metadata: + +```shell +rake dgidb:normalize:drug_approval_types +rake dgidb:normalize:drug_types +rake dgidb:normalize:populate_source_counters +``` + ### Client setup Navigate to the [/client directory](/client): @@ -107,3 +221,15 @@ Start the client: ```shell yarn start ``` + +Frontend style is enforced by [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/). Conformance is ensured by [pre-commit](https://pre-commit.com/#usage). Before your first commit, run + +```shell +pre-commit install +``` + +In practice, Prettier will do most of the formatting work for you to be in accordance with ESLint. Run the following to autoformat a file: + +```shell +yarn run prettier --write path/to/file +``` diff --git a/client/.eslintrc.js b/client/.eslintrc.js new file mode 100644 index 00000000..1850f486 --- /dev/null +++ b/client/.eslintrc.js @@ -0,0 +1,39 @@ +// The env is development during local development, so it overrides all the rules to "warn" +// The env will be undefined for GH actions or when yarn lint-staged gets fired during pre-commit hooks, +// so we can still catch any violations there +const env = process.env.NODE_ENV; + +module.exports = { + env: { + browser: true, + es6: true, + }, + extends: ['react-app', 'react-app/jest', 'eslint:recommended'], + rules: { + 'react/react-in-jsx-scope': 'off', + 'import/no-unused-modules': 'off', + '@typescript-eslint/no-unused-vars': + env === 'development' ? 'warn' : 'error', + 'no-unused-vars': env === 'development' ? 'warn' : 'error', + }, + overrides: [ + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + '@typescript-eslint/no-unused-vars': [ + env === 'development' ? 'warn' : 'error', + { vars: 'all', args: 'after-used', ignoreRestSiblings: true }, + ], + }, + }, + { + files: ['**/*.js', '**/*.jsx'], + rules: { + 'no-unused-vars': [ + env === 'development' ? 'warn' : 'error', + { vars: 'all', args: 'after-used', ignoreRestSiblings: true }, + ], + }, + }, + ], +}; diff --git a/client/.husky/pre-commit b/client/.husky/pre-commit new file mode 100644 index 00000000..87ea02a1 --- /dev/null +++ b/client/.husky/pre-commit @@ -0,0 +1 @@ +npm run pre-commit diff --git a/client/.pre-commit-config.yaml b/client/.pre-commit-config.yaml new file mode 100644 index 00000000..ca7011d9 --- /dev/null +++ b/client/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + - id: detect-private-key + - id: trailing-whitespace + - id: end-of-file-fixer + - repo: local + hooks: + - id: lint-staged + name: lint-staged + entry: > + bash -c ' + cd client/src || exit 1 + yarn lint-staged + ' + language: system + files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx diff --git a/client/package.json b/client/package.json index f34e4d9b..30618dea 100644 --- a/client/package.json +++ b/client/package.json @@ -3,7 +3,6 @@ "version": "0.1.0", "private": true, "dependencies": { - "@ant-design/icons": "^4.7.0", "@apollo/client": "^3.4.10", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", @@ -31,6 +30,7 @@ "graphql-request": "^3.7.0", "graphql-tag": "^2.12.6", "js-yaml": "^4.1.0", + "lint-staged": "^15.2.4", "react": "^18.2.0", "react-chartjs-2": "^4.1.0", "react-copy-to-clipboard": "^5.1.0", @@ -49,10 +49,11 @@ }, "scripts": { "start": "env-cmd -f ./.env.local react-scripts start", - "build": "react-scripts build", + "build": "npm run lint && react-scripts build", "test": "jest", "eject": "react-scripts eject", - "lint": "eslint --fix --ext .js,.ts,.tsx ./src --ignore-path .gitignore", + "lint": "eslint --fix --ext .js,.ts,.tsx ./src --ignore-path .gitignore --config .eslintrc.js", + "lint:warn": "eslint . --ext .js,.jsx,.ts,.tsx", "prettier": "prettier --ignore-path .gitignore --write \"**/*.+(js|json|ts|tsx)\"", "prettier-check": "prettier -c --ignore-path .gitignore \"**/*.+(js|json|ts|tsx)\"", "format": "yarn run prettier -- --write", @@ -104,7 +105,66 @@ ] }, "eslintConfig": { - "extends": "react-app" + "env": { + "browser": true, + "es6": true + }, + "extends": [ + "react-app", + "react-app/jest", + "eslint:recommended" + ], + "rules": { + "react/react-in-jsx-scope": "off", + "import/no-unused-modules": "off", + "@typescript-eslint/no-unused-vars": "warn", + "no-unused-vars": "warn" + }, + "overrides": [ + { + "files": [ + "**/*.ts", + "**/*.tsx" + ], + "rules": { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true + } + ] + } + }, + { + "files": [ + "**/*.js", + "**/*.jsx" + ], + "rules": { + "no-unused-vars": [ + "warn", + { + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true + } + ] + } + } + ] + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "yarn lint", + "yarn prettier --write" + ] }, "prettier": { "singleQuote": true diff --git a/client/src/components/About/InteractionClaimTypes/TypesTable.tsx b/client/src/components/About/InteractionClaimTypes/TypesTable.tsx index d96dcee3..0d6e3a7c 100644 --- a/client/src/components/About/InteractionClaimTypes/TypesTable.tsx +++ b/client/src/components/About/InteractionClaimTypes/TypesTable.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { useGetInteractionClaimTypes } from 'hooks/queries/useGetInteractionClaimTypes'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; diff --git a/client/src/components/Browse/Categories/BrowseCategories.scss b/client/src/components/Browse/Categories/BrowseCategories.scss index 859351d5..be2032ca 100644 --- a/client/src/components/Browse/Categories/BrowseCategories.scss +++ b/client/src/components/Browse/Categories/BrowseCategories.scss @@ -5,40 +5,4 @@ .category-list { flex: 1; } - - .ant-collapse-content { - .ant-collapse-content-box { - padding: 0px !important; - } - } - - .ant-table-wrapper { - background-color: var(--background-light) !important; - } - - .ant-table-pagination .ant-pagination { - margin: 8px 0 !important; - } - - .ant-table-container table > thead > tr > th { - background-color: var(--background-light); - } -} - -.ant-checkbox-group { - display: flex; - flex-direction: column; -} - -.ant-checkbox-group-item, -.ant-checkbox-wrapper { - color: var(--text-content); -} - -.ant-collapse { - background-color: var(--background-content); - - .ant-collapse-header { - color: var(--text-content) !important; - } } diff --git a/client/src/components/Browse/Categories/BrowseCategories.tsx b/client/src/components/Browse/Categories/BrowseCategories.tsx index f77dfa80..ced3dd06 100644 --- a/client/src/components/Browse/Categories/BrowseCategories.tsx +++ b/client/src/components/Browse/Categories/BrowseCategories.tsx @@ -138,7 +138,7 @@ export const BrowseCategories: React.FC = () => { {renderedCategories?.map((cat: any, index: number) => { if (cat.geneCount) { return ( - + } diff --git a/client/src/components/Browse/Sources/BrowseSources.tsx b/client/src/components/Browse/Sources/BrowseSources.tsx index a7316f55..a61146de 100644 --- a/client/src/components/Browse/Sources/BrowseSources.tsx +++ b/client/src/components/Browse/Sources/BrowseSources.tsx @@ -128,7 +128,7 @@ export const BrowseSources = () => { License: - + {src.license} diff --git a/client/src/components/Drug/DrugCharts/DirectionalityDrug.tsx b/client/src/components/Drug/DrugCharts/DirectionalityDrug.tsx index 7f8b2cee..7e7938dd 100644 --- a/client/src/components/Drug/DrugCharts/DirectionalityDrug.tsx +++ b/client/src/components/Drug/DrugCharts/DirectionalityDrug.tsx @@ -37,8 +37,6 @@ export const DirectionalityDrug: React.FC = ({ data }) => { responsive: true, }; - const labels = ['Activating', 'Inhibiting', 'N/A']; - useEffect(() => { let countCopy = [0, 0, 0]; data?.forEach((drug: any) => { diff --git a/client/src/components/Drug/DrugRecord/DrugRecord.tsx b/client/src/components/Drug/DrugRecord/DrugRecord.tsx index da65c159..7315c705 100644 --- a/client/src/components/Drug/DrugRecord/DrugRecord.tsx +++ b/client/src/components/Drug/DrugRecord/DrugRecord.tsx @@ -17,7 +17,7 @@ import TableCell from '@mui/material/TableCell'; import Table from '@mui/material/Table'; // components -import { Alert, LinearProgress, Link } from '@mui/material'; +import { LinearProgress, Link } from '@mui/material'; import InteractionTable from 'components/Shared/InteractionTable/InteractionTable'; import { useGetDrugInteractions } from 'hooks/queries/useGetDrugInteractions'; import { generateXrefLink } from 'utils/generateXrefLink'; @@ -277,11 +277,6 @@ export const DrugRecord: React.FC = () => { ) : ( - - - We could not find any results for this drug. - - - + ); }; diff --git a/client/src/components/Drug/DrugSearchResults/DrugSearchResults.scss b/client/src/components/Drug/DrugSearchResults/DrugSearchResults.scss deleted file mode 100644 index a7411ba9..00000000 --- a/client/src/components/Drug/DrugSearchResults/DrugSearchResults.scss +++ /dev/null @@ -1,14 +0,0 @@ -.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn { - color: var(--text-content); - background-color: var(--background); -} - -.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab, -.ant-tabs-card > div > .ant-tabs-nav .ant-tabs-tab { - color: var(--text-content); - background-color: var(--background); -} - -.ant-tabs-tab.ant-tabs-tab-active { - border-bottom: none !important; -} diff --git a/client/src/components/Drug/DrugSearchResults/DrugSearchResults.tsx b/client/src/components/Drug/DrugSearchResults/DrugSearchResults.tsx index 19c01d86..8fe169f0 100644 --- a/client/src/components/Drug/DrugSearchResults/DrugSearchResults.tsx +++ b/client/src/components/Drug/DrugSearchResults/DrugSearchResults.tsx @@ -1,8 +1,8 @@ +import React from 'react'; import TabPanel from 'components/Shared/TabPanel/TabPanel'; import { DrugSummary } from '../DrugSummary'; import AmbiguousTermsSummary from 'components/Shared/AmbiguousTermsSummary/AmbiguousTermsSummary'; import { Box, Tab, Tabs } from '@mui/material'; -import './DrugSearchResults.scss'; import { GlobalClientContext } from 'stores/Global/GlobalClient'; import { useContext } from 'react'; import { useGetMatchedResults } from 'hooks/queries/useGetAmbiguousResults'; diff --git a/client/src/components/Drug/DrugSummary/DrugSummary.scss b/client/src/components/Drug/DrugSummary/DrugSummary.scss index 430a2dcf..d283ba4f 100644 --- a/client/src/components/Drug/DrugSummary/DrugSummary.scss +++ b/client/src/components/Drug/DrugSummary/DrugSummary.scss @@ -88,20 +88,6 @@ align-items: center; justify-content: left; - .ant-tabs { - width: 100%; - } - - .ant-tabs-tabpane { - padding-left: 100px; - div { - canvas { - height: 500px !important; - width: 500px !important; - } - } - } - .score-container { padding-left: 10px; } diff --git a/client/src/components/Drug/DrugSummary/DrugSummary.tsx b/client/src/components/Drug/DrugSummary/DrugSummary.tsx index de648678..d5b24a21 100644 --- a/client/src/components/Drug/DrugSummary/DrugSummary.tsx +++ b/client/src/components/Drug/DrugSummary/DrugSummary.tsx @@ -150,7 +150,7 @@ const SummaryInfoDrug: React.FC = ({ return (

Infographics

- {getWindowSize().innerWidth >= 1550 ? ( + {windowSize.innerWidth >= 1550 ? (
diff --git a/client/src/components/Gene/GeneCharts/DirectionalityGene.tsx b/client/src/components/Gene/GeneCharts/DirectionalityGene.tsx index 8d75393e..710bdbe6 100644 --- a/client/src/components/Gene/GeneCharts/DirectionalityGene.tsx +++ b/client/src/components/Gene/GeneCharts/DirectionalityGene.tsx @@ -37,8 +37,6 @@ export const DirectionalityGene: React.FC = ({ data }) => { responsive: true, }; - const labels = ['Activating', 'Inhibiting', 'N/A']; - useEffect(() => { let countCopy = [0, 0, 0]; data?.forEach((gene: any) => { diff --git a/client/src/components/Gene/GeneRecord/GeneRecord.tsx b/client/src/components/Gene/GeneRecord/GeneRecord.tsx index c940a5ce..5e40d4f1 100644 --- a/client/src/components/Gene/GeneRecord/GeneRecord.tsx +++ b/client/src/components/Gene/GeneRecord/GeneRecord.tsx @@ -15,7 +15,7 @@ import Table from '@mui/material/Table'; import TableRow from '@mui/material/TableRow'; import TableCell from '@mui/material/TableCell'; -import { Alert, LinearProgress, Link } from '@mui/material'; +import { LinearProgress, Link } from '@mui/material'; import { useGetGeneInteractions } from 'hooks/queries/useGetGeneInteractions'; import InteractionTable from 'components/Shared/InteractionTable/InteractionTable'; import { dropRedundantCites } from 'utils/dropRedundantCites'; @@ -275,12 +275,7 @@ export const GeneRecord: React.FC = () => { ) : ( - - - We could not find any results for this gene. - - - + ); }; diff --git a/client/src/components/Gene/GeneSearchResults/GeneSearchResults.scss b/client/src/components/Gene/GeneSearchResults/GeneSearchResults.scss deleted file mode 100644 index a7411ba9..00000000 --- a/client/src/components/Gene/GeneSearchResults/GeneSearchResults.scss +++ /dev/null @@ -1,14 +0,0 @@ -.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn { - color: var(--text-content); - background-color: var(--background); -} - -.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab, -.ant-tabs-card > div > .ant-tabs-nav .ant-tabs-tab { - color: var(--text-content); - background-color: var(--background); -} - -.ant-tabs-tab.ant-tabs-tab-active { - border-bottom: none !important; -} diff --git a/client/src/components/Gene/GeneSearchResults/GeneSearchResults.tsx b/client/src/components/Gene/GeneSearchResults/GeneSearchResults.tsx index 51b5761c..97d84f40 100644 --- a/client/src/components/Gene/GeneSearchResults/GeneSearchResults.tsx +++ b/client/src/components/Gene/GeneSearchResults/GeneSearchResults.tsx @@ -1,8 +1,8 @@ +import React from 'react'; import TabPanel from 'components/Shared/TabPanel/TabPanel'; import { GeneSummary } from '../GeneSummary'; import AmbiguousTermsSummary from 'components/Shared/AmbiguousTermsSummary/AmbiguousTermsSummary'; import { Box, CircularProgress, Icon, Tab, Tabs } from '@mui/material'; -import './GeneSearchResults.scss'; import { GlobalClientContext } from 'stores/Global/GlobalClient'; import { useContext } from 'react'; import { useGetMatchedResults } from 'hooks/queries/useGetAmbiguousResults'; diff --git a/client/src/components/Gene/GeneSummary/GeneSummary.scss b/client/src/components/Gene/GeneSummary/GeneSummary.scss index dc014b73..62e1d3eb 100644 --- a/client/src/components/Gene/GeneSummary/GeneSummary.scss +++ b/client/src/components/Gene/GeneSummary/GeneSummary.scss @@ -1,6 +1,6 @@ .gene-summary-container { color: var(--text-content); - margin: 15px; + margin: 0 15px; background-color: var(--background); display: flex; flex-direction: column; diff --git a/client/src/components/Gene/GeneSummary/GeneSummary.tsx b/client/src/components/Gene/GeneSummary/GeneSummary.tsx index 439f815c..5d5fde87 100644 --- a/client/src/components/Gene/GeneSummary/GeneSummary.tsx +++ b/client/src/components/Gene/GeneSummary/GeneSummary.tsx @@ -18,18 +18,9 @@ import './GeneSummary.scss'; import Box from '@mui/material/Box'; import InteractionTable from 'components/Shared/InteractionTable/InteractionTable'; import TableDownloader from 'components/Shared/TableDownloader/TableDownloader'; -import { - Accordion, - AccordionDetails, - AccordionSummary, - Alert, - IconButton, - Tab, - Tabs, -} from '@mui/material'; +import { Alert, IconButton, Tab, Tabs } from '@mui/material'; import TabPanel from 'components/Shared/TabPanel/TabPanel'; import { useGetIsMobile } from 'hooks/shared/useGetIsMobile'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import CloseIcon from '@mui/icons-material/Close'; ChartJS.register( @@ -158,7 +149,7 @@ const SummaryInfo: React.FC = ({ geneMatches, selectedGenes }) => { return (

Infographics

- {getWindowSize().innerWidth >= 1580 ? ( + {windowSize.innerWidth >= 1580 ? (
@@ -198,7 +189,6 @@ interface SummaryProps { } export const GeneSummary: React.FC = ({ genes, isLoading }) => { - const isMobile = useGetIsMobile(); const [interactionResults, setInteractionResults] = useState([]); const [selectedGenes, setSelectedGenes] = useState([]); const [displayedInteractionResults, setDisplayedInteractionResults] = diff --git a/client/src/components/GeneCategories/AmbiguousMatchCard/AmbiguousMatchCard.tsx b/client/src/components/GeneCategories/AmbiguousMatchCard/AmbiguousMatchCard.tsx index cea52587..38fb5c53 100644 --- a/client/src/components/GeneCategories/AmbiguousMatchCard/AmbiguousMatchCard.tsx +++ b/client/src/components/GeneCategories/AmbiguousMatchCard/AmbiguousMatchCard.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Accordion, diff --git a/client/src/components/GeneCategories/AmbiguousMatches/AmbiguousMatches.tsx b/client/src/components/GeneCategories/AmbiguousMatches/AmbiguousMatches.tsx index 6e3370cd..45e1e23e 100644 --- a/client/src/components/GeneCategories/AmbiguousMatches/AmbiguousMatches.tsx +++ b/client/src/components/GeneCategories/AmbiguousMatches/AmbiguousMatches.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Box, Grid, Paper, Typography } from '@mui/material'; import { ErrorMessage } from 'components/Shared/ErrorMessage/ErrorMessage'; import { LoadingSpinner } from 'components/Shared/LoadingSpinner/LoadingSpinner'; diff --git a/client/src/components/GeneCategories/DirectMatchCard/DirectMatchCard.tsx b/client/src/components/GeneCategories/DirectMatchCard/DirectMatchCard.tsx index 2d85719b..baa842c1 100644 --- a/client/src/components/GeneCategories/DirectMatchCard/DirectMatchCard.tsx +++ b/client/src/components/GeneCategories/DirectMatchCard/DirectMatchCard.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Accordion, AccordionDetails, diff --git a/client/src/components/GeneCategories/DirectMatches/DirectMatches.tsx b/client/src/components/GeneCategories/DirectMatches/DirectMatches.tsx index a7dc7b89..bbab3308 100644 --- a/client/src/components/GeneCategories/DirectMatches/DirectMatches.tsx +++ b/client/src/components/GeneCategories/DirectMatches/DirectMatches.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Box, Grid, Typography } from '@mui/material'; import { ErrorMessage } from 'components/Shared/ErrorMessage/ErrorMessage'; import TableDownloader from 'components/Shared/TableDownloader/TableDownloader'; diff --git a/client/src/components/GeneCategories/GeneCategoriesSearchResults/GeneCategoriesSearchResults.tsx b/client/src/components/GeneCategories/GeneCategoriesSearchResults/GeneCategoriesSearchResults.tsx index 0a49689f..2c5a21b0 100644 --- a/client/src/components/GeneCategories/GeneCategoriesSearchResults/GeneCategoriesSearchResults.tsx +++ b/client/src/components/GeneCategories/GeneCategoriesSearchResults/GeneCategoriesSearchResults.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Tab, Tabs } from '@mui/material'; import TabPanel from 'components/Shared/TabPanel/TabPanel'; import { useGetCategories } from 'hooks/queries/useGetCategories'; diff --git a/client/src/components/Interaction/InteractionRecord/InteractionRecord.tsx b/client/src/components/Interaction/InteractionRecord/InteractionRecord.tsx index e344b880..0545e395 100644 --- a/client/src/components/Interaction/InteractionRecord/InteractionRecord.tsx +++ b/client/src/components/Interaction/InteractionRecord/InteractionRecord.tsx @@ -16,7 +16,6 @@ import AccordionDetails from '@mui/material/AccordionDetails'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ArrowRightIcon from '@mui/icons-material/ArrowRight'; import { truncateDecimals } from 'utils/format'; -import { Alert } from '@mui/material'; import { NotFoundError } from 'components/Shared/NotFoundError/NotFoundError'; import { useGetIsMobile } from 'hooks/shared/useGetIsMobile'; @@ -245,12 +244,7 @@ export const InteractionRecord: React.FC = () => { ) : ( - - - We could not find any results for this interaction. - - - + ); }; diff --git a/client/src/components/Layout/MainLayout.scss b/client/src/components/Layout/MainLayout.scss index 754399e2..9ad1ee20 100644 --- a/client/src/components/Layout/MainLayout.scss +++ b/client/src/components/Layout/MainLayout.scss @@ -99,15 +99,6 @@ header { } } } - - .ant-menu-title-content { - color: var(--text-content); - } - - .ant-menu-dark.ant-menu-horizontal { - background-color: var(--background); - color: var(--text-content); - } } footer { @@ -124,8 +115,3 @@ footer { position: absolute; bottom: 0; } - -.ant-tabs { - background-color: var(--background); - border-radius: 5px; -} diff --git a/client/src/components/Layout/MainLayout.tsx b/client/src/components/Layout/MainLayout.tsx index 62d748dc..6f6ee528 100644 --- a/client/src/components/Layout/MainLayout.tsx +++ b/client/src/components/Layout/MainLayout.tsx @@ -17,7 +17,6 @@ import { DialogTitle, Drawer, IconButton, - Link, List, ListItem, ListItemButton, @@ -41,9 +40,6 @@ type MainLayoutProps = { const Header: React.FC = () => { const [anchorEl, setAnchorEl] = useState(null); - const [hideBanner, setHideBanner] = useState( - window.localStorage.getItem('banner-closed') - ); const [showMenuDrawer, setShowMenuDrawer] = useState(false); const handleOpen = (event: any) => { @@ -54,11 +50,6 @@ const Header: React.FC = () => { setAnchorEl(null); }; - const handleCloseNotificationBanner = () => { - setHideBanner('true'); - window.localStorage.setItem('banner-closed', 'true'); - }; - const navigate = useNavigate(); const isMobile = useGetIsMobile(); @@ -169,26 +160,6 @@ const Header: React.FC = () => {
{isMobile ? mobileNavMenu : desktopNavMenu} - {!hideBanner ? ( - - - The previous version of DGIdb can be found at{' '} - - old.dgidb.org - {' '} - until June 1st, 2024. - - - - - - ) : ( - '' - )} ); }; diff --git a/client/src/components/Shared/AmbiguousTermsSummary/AmbiguousResult.tsx b/client/src/components/Shared/AmbiguousTermsSummary/AmbiguousResult.tsx index 92f33456..e49369e8 100644 --- a/client/src/components/Shared/AmbiguousTermsSummary/AmbiguousResult.tsx +++ b/client/src/components/Shared/AmbiguousTermsSummary/AmbiguousResult.tsx @@ -20,10 +20,7 @@ interface Props { resultType: string; } -export const AmbiguousResult: React.FC = ({ - ambiguousTermData, - resultType, -}) => { +export const AmbiguousResult: React.FC = ({ ambiguousTermData }) => { const [selectedTerm, setSelectedTerm] = useState([]); const [interactionResults, setInteractionResults] = useState([]); const handleChange = (event: SelectChangeEvent) => { diff --git a/client/src/components/Shared/Buttons/ButtonAction.tsx b/client/src/components/Shared/Buttons/ButtonAction.tsx index 20cf3919..5bde4251 100644 --- a/client/src/components/Shared/Buttons/ButtonAction.tsx +++ b/client/src/components/Shared/Buttons/ButtonAction.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import './ButtonAction.scss'; interface Props { diff --git a/client/src/components/Shared/ErrorMessage/ErrorMessage.tsx b/client/src/components/Shared/ErrorMessage/ErrorMessage.tsx index abc707f0..83f6dcca 100644 --- a/client/src/components/Shared/ErrorMessage/ErrorMessage.tsx +++ b/client/src/components/Shared/ErrorMessage/ErrorMessage.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Link, Typography } from '@mui/material'; export const ErrorMessage: React.FC = () => ( diff --git a/client/src/components/Shared/NotFoundError/NotFoundError.tsx b/client/src/components/Shared/NotFoundError/NotFoundError.tsx index 3b53952a..64fda865 100644 --- a/client/src/components/Shared/NotFoundError/NotFoundError.tsx +++ b/client/src/components/Shared/NotFoundError/NotFoundError.tsx @@ -1,9 +1,31 @@ -import { Box } from '@mui/material'; +import React from 'react'; +import { Alert, Box, Link } from '@mui/material'; import './NotFoundError.scss'; +import { useNavigate } from 'react-router-dom'; + +interface Props { + errorMessage: string; +} + +export const NotFoundError: React.FC = ({ errorMessage }) => { + const navigate = useNavigate(); -export const NotFoundError: React.FC = () => { return ( - + + + {errorMessage} + + navigate(-1)} + sx={{ cursor: 'pointer', marginBottom: '15px' }} + > + Click here to go back to the previous page. + + + Click here to go to the home page. + + + 404 error { DGIdb {currentRelease.name} ( {new Date(currentRelease.published_at).toLocaleString().split(',')[0]} )  •   - + Release Notes   •   - + History
diff --git a/client/src/components/Shared/SearchBar/SearchBar.tsx b/client/src/components/Shared/SearchBar/SearchBar.tsx index 62a084fd..846abb1e 100644 --- a/client/src/components/Shared/SearchBar/SearchBar.tsx +++ b/client/src/components/Shared/SearchBar/SearchBar.tsx @@ -30,6 +30,12 @@ enum DelimiterTypes { TabNewline = 'Tab or Newline', } +enum DelimiterTypes { + Comma = 'Comma', + CommaSpace = 'Comma With Space', + TabNewline = 'Tab or Newline', +} + type SearchBarProps = { handleSubmit: () => void; }; @@ -148,7 +154,74 @@ const SearchBar: React.FC = ({ handleSubmit }) => { }) ); } - }, [selectedOptions]); + }, [selectedOptions, state.searchTerms]); + + const convertToDropdownOptions = (options: string[]) => { + return options.map((item: string) => { + return { suggestion: item.trim() }; + }); + }; + + const handlePaste = (event: any) => { + let pastedText = event.clipboardData.getData('text'); + let pastedOptions: any[] = convertToDropdownOptions([pastedText]); + + const commaSepOptions = pastedText.split(','); + + if (pastedSearchDelimiter === DelimiterTypes.Comma) { + pastedOptions = convertToDropdownOptions(commaSepOptions); + } else if (pastedSearchDelimiter === DelimiterTypes.CommaSpace) { + const commaSpaceSepOptions = pastedText.split(', '); + pastedOptions = convertToDropdownOptions(commaSpaceSepOptions); + } else if (pastedSearchDelimiter === DelimiterTypes.TabNewline) { + const whitespaceRegex = /[\t\n\r\f\v]/; + const whitespaceSepOptions = pastedText.split(whitespaceRegex); + pastedOptions = convertToDropdownOptions(whitespaceSepOptions); + } else { + pastedOptions = convertToDropdownOptions(commaSepOptions); + } + setSearchWasPasted(true); + // make sure we persist the search terms already entered, combine any pre-existing search terms with the new pasted options + const newSearchOptions = selectedOptions.concat(pastedOptions); + // remove any duplicated terms (need to iterate through only the terms since objects are never equivalent in js, even if the contents are the same) + const uniqueSearchTerms = [ + ...new Set(newSearchOptions.map((option) => option.suggestion)), + ]; + setSelectedOptions(convertToDropdownOptions(uniqueSearchTerms)); + // we don't want the code to also run what's in onInputChange for the Autocomplete since everything is handled here + event.preventDefault(); + }; + + const handleCheckboxSelect = (event: any) => { + setPastingFromDocument(event.target.checked); + // reset the selected delimiter and searchWasPasted, to avoid potential weird behaviors if a user deselects the checkbox + setPastedSearchDelimiter(''); + setSearchWasPasted(false); + }; + + const handleDelimiterChange = (event: any) => { + setPastedSearchDelimiter(event.target.value as string); + }; + + const pasteAlert = + searchWasPasted && pastedSearchDelimiter === '' ? ( + + + Verify your search terms +

+ It looks like you pasted search terms. We have defaulted the + delimiter to comma-separated terms. +

+

+ If this is incorrect or you would like to use a different delimiter, + make sure to check the “Bulk search” option below and select a + delimiter from the drop down. +

+
+
+ ) : ( + <> + ); const convertToDropdownOptions = (options: string[]) => { return options.map((item: string) => { diff --git a/client/src/components/Shared/TabPanel/TabPanel.tsx b/client/src/components/Shared/TabPanel/TabPanel.tsx index 8422849a..b44419ff 100644 --- a/client/src/components/Shared/TabPanel/TabPanel.tsx +++ b/client/src/components/Shared/TabPanel/TabPanel.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; interface TabPanelProps { diff --git a/client/src/pages/API/API.tsx b/client/src/pages/API/API.tsx index c1256292..dd57fa84 100644 --- a/client/src/pages/API/API.tsx +++ b/client/src/pages/API/API.tsx @@ -25,9 +25,6 @@ const buttonStyle = { border: 'none', fontSize: '10px', }; -const defaultStyle = { - backgroundColor: '#f3e5f5', -}; // queries const query1 = ` diff --git a/client/src/pages/About/SubSections/Overview.tsx b/client/src/pages/About/SubSections/Overview.tsx index 358d0cd7..a031b019 100644 --- a/client/src/pages/About/SubSections/Overview.tsx +++ b/client/src/pages/About/SubSections/Overview.tsx @@ -9,16 +9,17 @@ export const Overview = () => {

- Integration of the Drug-Gene Interaction Database (DGIdb 4.0) with - open crowdsource efforts. + DGIdb 5.0: rebuilding the drug-gene interaction database for precision + medicine and drug discovery platforms {' '} - Freshour S*, Kiwala S*, Cotto KC*, Coffman AC, McMichael JF, Song J, - Griffith M, Griffith OL, Wagner AH. Nucleic Acids Research. 2020 Nov 25; - doi: https://doi.org/10.1093/nar/gkaa1084. PMID: 33237278 + Cannon M, Stevenson J, Stahl K, Basu R, Coffman A, Kiwala S, McMichael + JF, Kuzma K, Morrisey D, Cotto KC, Mardis ER, Griffith OL, Griffith M, + Wagner AH. Nucleic Acids Research. 2024 Jan 5; doi: + https://doi.org/10.1093/nar/gkad1040. PMID: 37953380.

@@ -110,10 +111,10 @@ export const Overview = () => { > Therapy {' '} - Normalizer services. DGIdb contains over 10,000 genes and 15,000 drugs - involved in over 50,000 drug-gene interactions or belonging to one of 43 - potentially druggable gene categories. Users can enter a list of genes - to retrieve all known or potentially druggable genes in that list. + Normalizer services. DGIdb contains over 10,000 genes and 20,000 drugs + involved in nearly 70,000 drug-gene interactions or belonging to one of + 43 potentially druggable gene categories. Users can enter a list of + genes to retrieve all known or potentially druggable genes in that list. Results can be filtered by source, interaction type, or gene category. DGIdb is built on Ruby on Rails and PostgreSQL with a flexible relational database schema to accommodate metadata from various sources. diff --git a/client/src/pages/Browse/Browse.tsx b/client/src/pages/Browse/Browse.tsx index 645560d4..bd0b1ea9 100644 --- a/client/src/pages/Browse/Browse.tsx +++ b/client/src/pages/Browse/Browse.tsx @@ -1,5 +1,4 @@ // hooks/dependencies -import React, { useState, useContext, useEffect } from 'react'; import { BrowseCategories } from 'components/Browse/Categories'; // styles diff --git a/client/src/pages/Downloads/Files/Files.tsx b/client/src/pages/Downloads/Files/Files.tsx index 5aa5c107..58389f66 100644 --- a/client/src/pages/Downloads/Files/Files.tsx +++ b/client/src/pages/Downloads/Files/Files.tsx @@ -25,6 +25,7 @@ function getDataObj(date: string) { const rows = [ getDataObj('latest'), + getDataObj('2023-Dec'), getDataObj('2022-Feb'), getDataObj('2021-May'), getDataObj('2021-Jan'), diff --git a/client/src/routes/public.tsx b/client/src/routes/public.tsx index f57a964c..cf9d13d4 100644 --- a/client/src/routes/public.tsx +++ b/client/src/routes/public.tsx @@ -1,5 +1,5 @@ import { Suspense, useEffect } from 'react'; -import { Navigate, Outlet, useLocation, useRoutes } from 'react-router-dom'; +import { Outlet, useLocation, useRoutes } from 'react-router-dom'; import { Home } from 'pages/Home'; import { Results } from 'pages/Results'; @@ -13,6 +13,7 @@ import { About } from 'pages/About'; import { Downloads } from 'pages/Downloads'; import { API } from 'pages/API'; import { InteractionRecord } from 'components/Interaction/InteractionRecord'; +import { NotFoundError } from 'components/Shared/NotFoundError/NotFoundError'; const App = () => { const { pathname, hash, key } = useLocation(); @@ -93,7 +94,12 @@ export const Routes = () => { { path: '/downloads', element: }, { path: '/api', element: }, { path: '/', element: }, - { path: '*', element: }, + { + path: '*', + element: ( + + ), + }, ], }, ]; diff --git a/client/yarn.lock b/client/yarn.lock index 8099b9c4..21b24392 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -9,29 +9,6 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" -"@ant-design/colors@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298" - integrity sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ== - dependencies: - "@ctrl/tinycolor" "^3.4.0" - -"@ant-design/icons-svg@^4.2.1": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz#8630da8eb4471a4aabdaed7d1ff6a97dcb2cf05a" - integrity sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw== - -"@ant-design/icons@^4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.7.0.tgz#8c3cbe0a556ba92af5dc7d1e70c0b25b5179af0f" - integrity sha512-aoB4Z7JA431rt6d4u+8xcNPPCrdufSRMUOpxa1ab6mz1JCQZOEVolj2WVs/tDFmN62zzK30mNelEsprLYsSF3g== - dependencies: - "@ant-design/colors" "^6.0.0" - "@ant-design/icons-svg" "^4.2.1" - "@babel/runtime" "^7.11.2" - classnames "^2.2.6" - rc-util "^5.9.4" - "@apideck/better-ajv-errors@^0.3.1": version "0.3.3" resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.3.tgz#ab0b1e981e1749bf59736cf7ebe25cfc9f949c15" @@ -2002,11 +1979,6 @@ dependencies: postcss-value-parser "^4.2.0" -"@ctrl/tinycolor@^3.4.0": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz#75b4c27948c81e88ccd3a8902047bcd797f38d32" - integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw== - "@dabh/diagnostics@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" @@ -3810,6 +3782,11 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: dependencies: type-fest "^0.11.0" +ansi-escapes@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" + integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== + ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" @@ -3859,6 +3836,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.0.0, ansi-styles@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + anymatch@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" @@ -4257,6 +4239,13 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + broadcast-channel@^3.4.1: version "3.7.0" resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" @@ -4401,6 +4390,11 @@ case-sensitive-paths-webpack-plugin@^2.4.0: resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== +chalk@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -4506,11 +4500,6 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -classnames@^2.2.6: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== - clean-css@^5.2.2: version "5.3.0" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.0.tgz#ad3d8238d5f3549e83d5f87205189494bc7cbb59" @@ -4523,6 +4512,21 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + cliui@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" @@ -4637,6 +4641,11 @@ colorette@^2.0.10: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + colorspace@1.1.x: version "1.1.4" resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" @@ -4652,6 +4661,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -5132,7 +5146,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -5486,6 +5500,11 @@ emittery@^0.8.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== +emoji-regex@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -6048,11 +6067,31 @@ eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +execa@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -6240,6 +6279,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -6448,6 +6494,11 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" + integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" @@ -6484,6 +6535,11 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -6875,6 +6931,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + husky@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" @@ -7125,6 +7186,18 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + is-generator-fn@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" @@ -7249,6 +7322,11 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" @@ -8025,6 +8103,11 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lilconfig@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.1.tgz#9d8a246fa753106cfc205fd2d77042faca56e5e3" + integrity sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ== + lilconfig@^2.0.3, lilconfig@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" @@ -8042,6 +8125,34 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" +lint-staged@^15.2.4: + version "15.2.4" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.4.tgz#b8376b459a308d40e5dfdae302f333fa4c4bf126" + integrity sha512-3F9KRQIS2fVDGtCkBp4Bx0jswjX7zUcKx6OF0ZeY1prksUyKPRIIUqZhIUYAstJfvj6i48VFs4dwVIbCYwvTYQ== + dependencies: + chalk "5.3.0" + commander "12.1.0" + debug "4.3.4" + execa "8.0.1" + lilconfig "3.1.1" + listr2 "8.2.1" + micromatch "4.0.6" + pidtree "0.6.0" + string-argv "0.3.2" + yaml "2.4.2" + +listr2@8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.1.tgz#06a1a6efe85f23c5324180d7c1ddbd96b5eefd6d" + integrity sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g== + dependencies: + cli-truncate "^4.0.0" + colorette "^2.0.20" + eventemitter3 "^5.0.1" + log-update "^6.0.0" + rfdc "^1.3.1" + wrap-ansi "^9.0.0" + loader-runner@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" @@ -8121,6 +8232,17 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-update@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" + integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== + dependencies: + ansi-escapes "^6.2.0" + cli-cursor "^4.0.0" + slice-ansi "^7.0.0" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + logform@^2.3.2, logform@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" @@ -8266,6 +8388,14 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +micromatch@4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.6.tgz#ab4e37c42726b9cd788181ba4a2a4fead8e394a3" + integrity sha512-Y4Ypn3oujJYxJcMacVgcs92wofTHxp9FzfDpQON4msDefoC0lb3ETvQLOdLcbhSwU1bz8HrL/1sygfBIHudrkQ== + dependencies: + braces "^3.0.3" + picomatch "^4.0.2" + micromatch@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" @@ -8321,6 +8451,11 @@ mimic-fn@^2.0.0, mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -8503,6 +8638,13 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -8665,13 +8807,20 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + open@^8.0.9, open@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" @@ -8879,6 +9028,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -8924,6 +9078,16 @@ picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +pidtree@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + pirates@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" @@ -9683,15 +9847,6 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -rc-util@^5.9.4: - version "5.24.4" - resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.24.4.tgz#a4126f01358c86f17c1bf380a1d83d6c9155ae65" - integrity sha512-2a4RQnycV9eV7lVZPEJ7QwJRPlZNc06J7CwcwZo4vIHr3PfUqtYgl1EkUV9ETAc6VRRi8XZOMFhYG63whlIC9Q== - dependencies: - "@babel/runtime" "^7.18.3" - react-is "^16.12.0" - shallowequal "^1.1.0" - react-app-polyfill@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz#95221e0a9bd259e5ca6b177c7bb1cb6768f68fd7" @@ -9789,7 +9944,7 @@ react-hook-form@^7.14.1: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.14.1.tgz#e52d346d1e436f299cd6d08598b6f2d2edcc7ee9" integrity sha512-NcKsxUokZhvpKBdxjnpIn/9v6IlmML63I3PI5aWlnI75ptswYvQeIaaOK8fMohr9Z2HZeeiYBjYDlv+ooffI4Q== -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -10229,6 +10384,14 @@ resolve@^2.0.0-next.3: is-core-module "^2.2.0" path-parse "^1.0.6" +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -10239,6 +10402,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" + integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== + rimraf@2, rimraf@^2.5.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -10502,11 +10670,6 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -shallowequal@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -10555,6 +10718,11 @@ signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -10577,6 +10745,22 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slice-ansi@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + sockjs@^0.3.21: version "0.3.21" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417" @@ -10718,6 +10902,11 @@ stackframe@^1.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +string-argv@0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" + integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== + string-length@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" @@ -10765,6 +10954,15 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a" + integrity sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string.prototype.matchall@^4.0.6: version "4.0.7" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" @@ -10874,6 +11072,13 @@ strip-ansi@^7.0.0, strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -10899,6 +11104,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -12042,6 +12252,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -12097,6 +12316,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362" + integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA== + yaml@^1.10.0, yaml@^1.7.2: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" diff --git a/server/app/graphql/resolvers/genes.rb b/server/app/graphql/resolvers/genes.rb index 4f0af7e1..1063515c 100644 --- a/server/app/graphql/resolvers/genes.rb +++ b/server/app/graphql/resolvers/genes.rb @@ -32,7 +32,8 @@ class Resolvers::Genes < GraphQL::Schema::Resolver # gene claim category by name option(:gene_claim_category, type: [String], description: "Filtering on gene claim category name.") do |scope, values| - scope.joins(gene_claims: :gene_claim_categories).where('gene_claim_categories.name IN (?)', values) + lowercase_values = values.map(&:downcase) + scope.joins(gene_claims: :gene_claim_categories).where('LOWER(gene_claim_categories.name) IN (?)', lowercase_values) end option(:interaction_type, type: String, description: 'Exact filtering on interaction claim type.') do |scope, value| diff --git a/server/app/graphql/resolvers/interactions.rb b/server/app/graphql/resolvers/interactions.rb new file mode 100644 index 00000000..1673209f --- /dev/null +++ b/server/app/graphql/resolvers/interactions.rb @@ -0,0 +1,62 @@ +require 'search_object' +require 'search_object/plugin/graphql' + +class Resolvers::Interactions < GraphQL::Schema::Resolver + include SearchObject.module(:graphql) + + type Types::InteractionType.connection_type, null: true + + scope { Interaction.all } + + option(:drug_names, type: [String], description: 'Filters interactions to only include those involving any of the specified drug names. This field accepts a list of drug names, which are case-insensitive, and returns interactions where the associated drug\'s name matches any name in the provided list.') do |scope, value| + if drug_names.nil? || drug_names.length == 0 + scope + else + names = value.map { |v| v.upcase } + scope.joins(:drug).where(drugs: {name: names}) + end + end + + option(:gene_names, type: [String], description: 'Filters interactions to only include those involving any of the specified gene names. This field accepts a list of gene names, which are case-insensitive, and returns interactions where the gene\'s name matches any name in the provided list.') do |scope, value| + if gene_names.nil? || gene_names.length == 0 + scope + else + names = value.map { |v| v.upcase } + scope.joins(:gene).where(genes: {name: names}) + end + end + + option(:drug_concept_ids, type: [String], description: 'Filters interactions to only include those involving any of the specified drug concept IDs. This field accepts a list of drug concept IDs, which are case-insensitive, and returns interactions where the associated drug\'s concept ID matches any ID in the provided list.') do |scope, value| + if drug_concept_ids.nil? || drug_concept_ids.length == 0 + scope + else + concept_ids = value.map { |v| v.upcase } + scope.joins(:drug).where(drugs: {concept_id: concept_ids}) + end + end + + option(:gene_concept_ids, type: [String], description: 'Filters interactions to only include those involving any of the specified gene concept IDs. This field accepts a list of gene concept IDs, which are case-insensitive, and returns interactions where the associated gene\'s concept ID matches any ID in the provided list.') do |scope, value| + if gene_concept_ids.nil? || gene_concept_ids.length == 0 + scope + else + concept_ids = value.map { |v| v.upcase } + scope.joins(:gene).where(genes: {concept_id: concept_ids}) + end + end + + option(:approved, type: Boolean, description: 'Filters interactions to include only those involving drugs with the specified approval status. Setting this to `true` returns interactions associated with approved drugs, while `false` returns interactions with drugs that are not known to be approved') do |scope, value| + scope.joins(:drug).where(drugs: {approved: value}) + end + + option(:immunotherapy, type: Boolean, description: 'Filters interactions based on whether the involved drugs are classified as immunotherapies. Set this to `true` to retrieve interactions involving immunotherapy drugs, or `false` to retrieve interactions involving non-immunotherapeutics') do |scope, value| + scope.joins(:drug).where(drugs: {immunotherapy: value}) + end + + option(:anti_neoplastic, type: Boolean, description: 'Filters interactions based on whether the involved drugs are classified as antineoplastics. Use `true` to include interactions involving antineoplastic drugs, or `false` to include those involving non-antineoplastic drugs') do |scope, value| + scope.joins(:drug).where(drugs: {anti_neoplastic: value}) + end + + option(:sources, type: [String], description: 'Filters interactions to include only those provided by the specified sources. This filter accepts a list of source names and returns interactions that have at least one matching source in the list') do |scope, value| + scope.joins(:sources).where(sources: {source_db_name: value}) + end +end diff --git a/server/app/graphql/types/query_type.rb b/server/app/graphql/types/query_type.rb index b04b5eae..81c95910 100644 --- a/server/app/graphql/types/query_type.rb +++ b/server/app/graphql/types/query_type.rb @@ -12,6 +12,9 @@ class QueryType < Types::BaseObject field :sources, resolver: Resolvers::Sources field :categories, resolver: Resolvers::Categories field :interaction_claim_types, resolver: Resolvers::InteractionClaimTypes + field :interactions, resolver: Resolvers::Interactions + + field :service_info, Types::MetaType, null: false field :service_info, Types::MetaType, null: false @@ -317,7 +320,7 @@ def interaction_claim(id:) end field :interaction, Types::InteractionType, null: true do - description "An interaction" + description "An interaction between a drug and a gene" argument :id, ID, required: true end diff --git a/server/lib/genome/groupers/base.rb b/server/lib/genome/groupers/base.rb index 58c73ad9..31d15395 100644 --- a/server/lib/genome/groupers/base.rb +++ b/server/lib/genome/groupers/base.rb @@ -25,9 +25,9 @@ def fetch_json_response(url) end def fetch_source_meta - url = URI("#{@normalizer_url_root}search?q=") + url = URI("#{@normalizer_host}search?q=") body = fetch_json_response(url) - body['source_matches'].reduce({}) { |map, source| map.update(source['source'] => source['source_meta_']) } + body['source_matches'].transform_values { |value| value['source_meta_'] } end # Normalize claim terms @@ -60,7 +60,7 @@ def normalize_claim(primary_term, claim_aliases) response = retrieve_normalizer_response(claim_alias.alias) match_type = response['match_type'] if !response.nil? && match_type > 0 - concept_id = response[@descriptor_name][@id_name] + concept_id = response['normalized_id'] if !claim_responses.key?(concept_id) claim_responses[concept_id] = response end @@ -93,6 +93,10 @@ def normalize_claim(primary_term, claim_aliases) response end + def get_concept_id(response) + response['normalized_id'] unless response['match_type'].zero? + end + def retrieve_extension(descriptor, type, default = nil) unless descriptor.fetch('extensions').blank? descriptor['extensions'].each do |extension| @@ -103,7 +107,7 @@ def retrieve_extension(descriptor, type, default = nil) end def retrieve_normalizer_response(term) - body = fetch_json_response("#{@normalizer_url_root}normalize?q=#{CGI.escape(term)}") + body = fetch_json_response("#{@normalizer_host}normalize?q=#{CGI.escape(term)}") @term_to_match_dict[term.upcase] = get_concept_id(body) unless term == '' || body.nil? body @@ -114,7 +118,7 @@ def key_non_nil_match(term) end def retrieve_normalizer_data(term) - body = fetch_json_response("#{@normalizer_url_root}normalize_unmerged?q=#{CGI.escape(term)}") + body = fetch_json_response("#{@normalizer_host}normalize_unmerged?q=#{CGI.escape(term)}") body['source_matches'] end end diff --git a/server/lib/genome/groupers/drug_grouper.rb b/server/lib/genome/groupers/drug_grouper.rb index c9c20260..3acd2b66 100644 --- a/server/lib/genome/groupers/drug_grouper.rb +++ b/server/lib/genome/groupers/drug_grouper.rb @@ -4,8 +4,11 @@ class DrugGrouper < Genome::Groupers::Base attr_reader :term_to_match_dict def initialize - url_base = ENV['THERAPY_URL_BASE'] || 'http://localhost:8000' - @normalizer_url_root = "#{url_base}/therapy/" + url_base = ENV['THERAPY_HOSTNAME'] || 'http://localhost:8000' + if !url_base.ends_with? "/" + url_base += "/" + end + @normalizer_host = "#{url_base}therapy/" @term_to_match_dict = {} @@ -28,7 +31,6 @@ def run(source_id = nil) puts "Grouping #{claims.length} ungrouped drug claims from #{source_name}" end - set_response_structure create_sources @@ -40,8 +42,8 @@ def run(source_id = nil) if normalized_drug.is_a? String normalized_id = normalized_drug else - normalized_id = normalized_drug[@descriptor_name][@id_name] - create_new_drug(normalized_drug[@descriptor_name]) if Drug.find_by(concept_id: normalized_id).nil? + normalized_id = normalized_drug['normalized_id'] + create_new_drug(normalized_drug['therapeutic_agent'], normalized_id) if Drug.find_by(concept_id: normalized_id).nil? end add_claim_to_drug(drug_claim, normalized_id) @@ -49,19 +51,6 @@ def run(source_id = nil) end end - def set_response_structure - url = URI("#{@normalizer_url_root}search?q=") - body = fetch_json_response(url) - version = body['service_meta_']['version'] - if version < '0.4.0' - @descriptor_name = 'therapy_descriptor' - @id_name = 'therapy_id' - else - @descriptor_name = 'therapeutic_descriptor' - @id_name = 'therapeutic' - end - end - def create_sources drug_source_type = SourceType.find_by(type: 'drug') @@ -165,10 +154,6 @@ def create_sources } end - def get_concept_id(response) - response[@descriptor_name][@id_name] unless response['match_type'].zero? - end - def produce_concept_id_nomenclature(concept_id) case concept_id when /rxcui:/ @@ -289,8 +274,8 @@ def add_grouper_claim_aliases(claim, record) end end - def add_grouper_data(drug, descriptor) - drug_data = retrieve_normalizer_data(descriptor[@id_name]) + def add_grouper_data(drug, drug_response, concept_id) + drug_data = retrieve_normalizer_data(concept_id) drug_data.each do |source_name, source_data| next if %w[DrugBank ChEMBL GuideToPHARMACOLOGY].include?(source_name) @@ -306,15 +291,15 @@ def add_grouper_data(drug, descriptor) end end - def create_new_drug(descriptor) - name = if descriptor.fetch('label').blank? - descriptor[@id_name] + def create_new_drug(drug_response, concept_id) + name = if drug_response['label'].nil? || drug_response['label'].blank? + concept_id else - descriptor['label'] + drug_response['label'] end - drug = Drug.where(concept_id: descriptor[@id_name], name: name.upcase).first_or_create + drug = Drug.where(concept_id: concept_id, name: name.upcase).first_or_create - add_grouper_data(drug, descriptor) + add_grouper_data(drug, drug_response, concept_id) end def find_drug_attribute(drug_claim_attribute) diff --git a/server/lib/genome/groupers/gene_grouper.rb b/server/lib/genome/groupers/gene_grouper.rb index ae33ae87..ea4b35ea 100644 --- a/server/lib/genome/groupers/gene_grouper.rb +++ b/server/lib/genome/groupers/gene_grouper.rb @@ -4,8 +4,11 @@ class GeneGrouper < Genome::Groupers::Base attr_reader :term_to_match_dict def initialize - url_base = ENV['GENE_URL_BASE'] || 'http://localhost:8000' - @normalizer_url_root = "#{url_base}/gene/" + url_base = ENV['GENE_HOSTNAME'] || 'http://localhost:8000' + if !url_base.ends_with? "/" + url_base += "/" + end + @normalizer_host = "#{url_base}gene/" @term_to_match_dict = {} @sources = {} @@ -33,7 +36,6 @@ def run(source_id = nil) puts "Grouping #{claims.length} ungrouped gene claims from #{source_name}" end - set_response_structure create_sources pbar = ProgressBar.create(title: 'Grouping genes', total: claims.size, format: "%t: %p%% %a |%B|") @@ -44,8 +46,8 @@ def run(source_id = nil) if normalized_gene.is_a? String normalized_id = normalized_gene else - normalized_id = normalized_gene[@descriptor_name][@id_name] - create_new_gene normalized_gene[@descriptor_name] if Gene.find_by(concept_id: normalized_id).nil? + normalized_id = normalized_gene['normalized_id'] + create_new_gene(normalized_gene['gene'], normalized_id) if Gene.find_by(concept_id: normalized_id).nil? end add_claim_to_gene(gene_claim, normalized_id) @@ -53,19 +55,6 @@ def run(source_id = nil) end end - def set_response_structure - @descriptor_name = 'gene_descriptor' - - url = URI("#{@normalizer_url_root}search?q=") - body = fetch_json_response(url) - version = body['service_meta_']['version'] - if version < '0.2.0' - @id_name = 'gene_id' - else - @id_name = 'gene' - end - end - def create_sources gene_source_type = SourceType.find_by(type: 'gene') @@ -90,8 +79,8 @@ def create_sources source_db_version: source_meta['HGNC']['version'], base_url: 'https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/', site_url: 'https://www.genenames.org', - citation: 'Tweedie S, Braschi B, Gray K, Jones TEM, Seal RL, Yates B, Bruford EA. Genenames.org: the HGNC and VGNC resources in 2021. Nucleic Acids Res. 2021 Jan 8;49(D1):D939-D946. doi: 10.1093/nar/gkaa980. PMID: 33152070; PMCID: PMC7779007.', - citation_short: 'Tweedie S, et al. Genenames.org: the HGNC and VGNC resources in 2021. Nucleic Acids Res. 2021 Jan 8;49(D1):D939-D946.', + citation: 'Seal RL, Braschi B, Gray K, Jones TEM, Tweedie S, Haim-Vilmovsky L, Bruford EA. Genenames.org: the HGNC resources in 2023. Nucleic Acids Res. 2023 Jan 6;51(D1):D1003-D1009. doi: 10.1093/nar/gkac888. PMID: 36243972; PMCID: PMC9825485.', + citation_short: 'Seal RL, et al. Genenames.org: the HGNC resources in 2023. Nucleic Acids Res. 2023 Jan 6;51(D1):D1003-D1009.', pmid: '33152070', pmcid: 'PMC7779007', doi: '10.1093/nar/gkaa980', @@ -105,8 +94,8 @@ def create_sources source_db_version: source_meta['Ensembl']['version'], base_url: 'https://ensembl.org/Homo_sapiens/Gene/Summary?g=', site_url: 'https://ensembl.org', - citation: 'Cunningham F, Allen JE, Allen J, Alvarez-Jarreta J, Amode MR, Armean IM, Austine-Orimoloye O, Azov AG, Barnes I, Bennett R, Berry A, Bhai J, Bignell A, Billis K, Boddu S, Brooks L, Charkhchi M, Cummins C, Da Rin Fioretto L, Davidson C, Dodiya K, Donaldson S, El Houdaigui B, El Naboulsi T, Fatima R, Giron CG, Genez T, Martinez JG, Guijarro-Clarke C, Gymer A, Hardy M, Hollis Z, Hourlier T, Hunt T, Juettemann T, Kaikala V, Kay M, Lavidas I, Le T, Lemos D, Marugán JC, Mohanan S, Mushtaq A, Naven M, Ogeh DN, Parker A, Parton A, Perry M, Piližota I, Prosovetskaia I, Sakthivel MP, Salam AIA, Schmitt BM, Schuilenburg H, Sheppard D, Pérez-Silva JG, Stark W, Steed E, Sutinen K, Sukumaran R, Sumathipala D, Suner MM, Szpak M, Thormann A, Tricomi FF, Urbina-Gómez D, Veidenberg A, Walsh TA, Walts B, Willhoft N, Winterbottom A, Wass E, Chakiachvili M, Flint B, Frankish A, Giorgetti S, Haggerty L, Hunt SE, IIsley GR, Loveland JE, Martin FJ, Moore B, Mudge JM, Muffato M, Perry E, Ruffier M, Tate J, Thybert D, Trevanion SJ, Dyer S, Harrison PW, Howe KL, Yates AD, Zerbino DR, Flicek P. Ensembl 2022. Nucleic Acids Res. 2022 Jan 7;50(D1):D988-D995. doi: 10.1093/nar/gkab1049. PMID: 34791404; PMCID: PMC8728283.', - citation_short: 'Cunningham F, et al. Ensembl 2022. Nucleic Acids Res. 2022 Jan 7;50(D1):D988-D995.', + citation: 'Harrison PW, Amode MR, Austine-Orimoloye O, Azov AG, Barba M, Barnes I, Becker A, Bennett R, Berry A, Bhai J, Bhurji SK, Boddu S, Branco Lins PR, Brooks L, Ramaraju SB, Campbell LI, Martinez MC, Charkhchi M, Chougule K, Cockburn A, Davidson C, De Silva NH, Dodiya K, Donaldson S, El Houdaigui B, Naboulsi TE, Fatima R, Giron CG, Genez T, Grigoriadis D, Ghattaoraya GS, Martinez JG, Gurbich TA, Hardy M, Hollis Z, Hourlier T, Hunt T, Kay M, Kaykala V, Le T, Lemos D, Lodha D, Marques-Coelho D, Maslen G, Merino GA, Mirabueno LP, Mushtaq A, Hossain SN, Ogeh DN, Sakthivel MP, Parker A, Perry M, Piližota I, Poppleton D, Prosovetskaia I, Raj S, Pérez-Silva JG, Salam AIA, Saraf S, Saraiva-Agostinho N, Sheppard D, Sinha S, Sipos B, Sitnik V, Stark W, Steed E, Suner MM, Surapaneni L, Sutinen K, Tricomi FF, Urbina-Gómez D, Veidenberg A, Walsh TA, Ware D, Wass E, Willhoft NL, Allen J, Alvarez-Jarreta J, Chakiachvili M, Flint B, Giorgetti S, Haggerty L, Ilsley GR, Keatley J, Loveland JE, Moore B, Mudge JM, Naamati G, Tate J, Trevanion SJ, Winterbottom A, Frankish A, Hunt SE, Cunningham F, Dyer S, Finn RD, Martin FJ, Yates AD. Ensembl 2024. Nucleic Acids Res. 2024 Jan 5;52(D1):D891-D899. doi: 10.1093/nar/gkad1049. PMID: 37953337; PMCID: PMC10767893.', + citation_short: 'Harrison PW, et al. Ensembl 2024. Nucleic Acids Res. 2024 Jan 5;52(D1):D891-D899.', pmid: '34791404', pmcid: 'PMC8728283', doi: '10.1093/nar/gkab1049', @@ -130,10 +119,6 @@ def create_sources } end - def get_concept_id(response) - response[@descriptor_name][@id_name] unless response['match_type'].zero? - end - def create_gene_claim(record, source) GeneClaim.where( name: record['symbol'], @@ -208,8 +193,8 @@ def add_grouper_claim_attribute(claim, record) ) end - def add_grouper_data(gene, descriptor) - gene_data = retrieve_normalizer_data(descriptor[@id_name]) + def add_grouper_data(gene, descriptor, normalized_id) + gene_data = retrieve_normalizer_data(normalized_id) gene_data.each do |source_name, source_data| source = @sources[source_name.to_sym] @@ -223,19 +208,19 @@ def add_grouper_data(gene, descriptor) end end - def create_new_gene(descriptor) - name = if descriptor.fetch('label').blank? - descriptor[@id_name] + def create_new_gene(gene_response, normalized_id) + name = if gene_response.fetch('label').blank? + normalized_id else - descriptor['label'] + gene_response['label'] end gene = Gene.where( - concept_id: descriptor[@id_name], + concept_id: normalized_id, name: name, - long_name: retrieve_extension(descriptor, 'approved_name') + long_name: retrieve_extension(gene_response, 'approved_name') ).first_or_create - add_grouper_data(gene, descriptor) + add_grouper_data(gene, gene_response, normalized_id) end def add_claim_attributes(claim, gene) diff --git a/server/lib/genome/importers/api_importers/civic/api_client.rb b/server/lib/genome/importers/api_importers/civic/api_client.rb index 29e95aa7..ec742563 100644 --- a/server/lib/genome/importers/api_importers/civic/api_client.rb +++ b/server/lib/genome/importers/api_importers/civic/api_client.rb @@ -4,15 +4,36 @@ module Genome; module Importers; module ApiImporters; module Civic class ApiClient - def enumerate_variants - variant_edges = send_query + def enumerate_drugs + response = send_query(DrugsQuery) Enumerator.new do |y| - until variant_edges.empty? - variant_edges.each { |edge| y << edge } - variant_edges = send_query(variant_edges[-1].cursor) + while response.therapies.page_info.has_next_page === true do + response.therapies.edges.each { |edge| y << edge.node } + response = send_query(DrugsQuery, response.therapies.page_info.end_cursor) end + response.therapies.edges.each { |edge| y << edge.node } end + end + def enumerate_genes + response = send_query(GenesQuery) + Enumerator.new do |y| + while response.genes.page_info.has_next_page === true do + response.genes.edges.each { |edge| y << edge.node } + response = send_query(GenesQuery, response.genes.page_info.end_cursor) + end + response.genes.edges.each { |edge| y << edge.node } + end + end + def enumerate_evidence_items + response = send_query(InteractionsQuery) + Enumerator.new do |y| + while response.evidence_items.page_info.has_next_page === true do + response.evidence_items.edges.each { |edge| y << edge.node } + response = send_query(InteractionsQuery, response.evidence_items.page_info.end_cursor) + end + response.evidence_items.edges.each { |edge| y << edge.node } + end end private @@ -29,58 +50,89 @@ def headers(_context) Client = GraphQL::Client.new(schema: Schema, execute: HTTP) end - Query = CivicApi::Client.parse <<-GRAPHQL - query($after: String!){ - variants(first: 50, after: $after) { + DrugsQuery = CivicApi::Client.parse <<-GRAPHQL + query ($after: String!) { + therapies(first: 50, after: $after) { + pageInfo { + endCursor + hasNextPage + } edges { cursor node { - gene { - id - entrezId - officialName - name - geneAliases - } - molecularProfiles { - nodes { - variants { - id - } - evidenceItems { - nodes { - name - id - significance - evidenceType - evidenceLevel - evidenceDirection - evidenceRating - link - therapies { + id + name + ncitId + } + } + } + } + GRAPHQL + + GenesQuery = CivicApi::Client.parse <<-GRAPHQL + query ($after: String!) { + genes(first: 50, after: $after) { + pageInfo { + endCursor + hasNextPage + } + edges { + cursor + node { + id + name + entrezId + fullName + featureAliases + } + } + } + } + GRAPHQL + + InteractionsQuery = CivicApi::Client.parse <<-GRAPHQL + query ($after: String!) { + evidenceItems(first: 50, after: $after, evidenceType: PREDICTIVE) { + pageInfo { + endCursor + hasNextPage + } + edges { + node { + id + evidenceDirection + evidenceRating + status + significance + evidenceLevel + molecularProfile { + variants { + feature { + featureInstance { + __typename + ... on Gene { name - ncitId - id - } - source { - citation - citationId - pmcId - sourceType } } } } } + therapies { + name + } + source { + citationId + sourceType + } } } } } GRAPHQL - def send_query(cursor = '') - response = CivicApi::Client.query(Query, variables: { 'after': cursor }) - response.data.variants.edges + def send_query(query, cursor = '') + response = CivicApi::Client.query(query, variables: { 'after': cursor }) + response.data end end end; end; end; end diff --git a/server/lib/genome/importers/api_importers/civic/importer.rb b/server/lib/genome/importers/api_importers/civic/importer.rb index ba5479f3..daf9abd3 100644 --- a/server/lib/genome/importers/api_importers/civic/importer.rb +++ b/server/lib/genome/importers/api_importers/civic/importer.rb @@ -9,14 +9,95 @@ class Importer < Genome::Importers::Base def initialize @source_db_name = 'CIViC' + @api_client = ApiClient.new end def create_claims + create_drug_claims + create_gene_claims create_interaction_claims end private + # this should be rare -- maybe impossible under the latest schema changes? + def importable_drug?(drug) + drug.name.upcase != 'N/A' && !drug.name.include?(';') + end + + def importable_evidence_item?(ei) + [ + ei.evidence_direction == 'SUPPORTS', + ei.evidence_level != 'E', + ei.evidence_rating.present? && ei.evidence_rating > 2 + ].all? + end + + def create_drug_claims + @api_client.enumerate_drugs.each do |drug| + next if !importable_drug?(drug) + + dc = create_drug_claim(drug.name.upcase, DrugNomenclature::PRIMARY_NAME) + create_drug_claim_alias(dc, "civic.tid:#{drug.id}", DrugNomenclature::CIVIC_TID) + create_drug_claim_alias(dc, "ncit:#{drug.ncit_id}", DrugNomenclature::NCIT_ID) if drug.ncit_id + end + end + + def create_gene_claims + @api_client.enumerate_genes.each do |gene| + gc = create_gene_claim(gene.name.upcase, GeneNomenclature::SYMBOL) + create_gene_claim_alias(gc, "civic.gid:#{gene.id}", GeneNomenclature::CIVIC_GID) + create_gene_claim_alias(gc, "ncbi.gene:#{gene.entrez_id}", GeneNomenclature::NCBI_ID) if gene.entrez_id + create_gene_claim_alias(gc, gene.full_name, GeneNomenclature::NAME) if gene.full_name + gene.feature_aliases.uniq.reject { |n| n == gene.name }.each do |gene_alias| + create_gene_claim_alias(gc, gene_alias, GeneNomenclature::SYMBOL) + end + end + end + + def create_entries_for_evidence_item(ei, dc, gc) + ic = create_interaction_claim(gc, dc) + if ei.source.citation_id.present? && ei.source.source_type == 'PubMed' + create_interaction_claim_publication(ic, ei.source.citation_id) + end + create_interaction_claim_link(ic, "EID#{ei.id}", "https://civicdb.org/evidence/#{ei.id}") + end + + # current policy is to skip items with poor rating/level, anything that doesn't support the claim, + # or any item with >1 genes (e.g. a fusion) + def create_interaction_claims + @api_client.enumerate_evidence_items.each do |ei| + next if !importable_evidence_item?(ei) + + # retain molecular profiles consisting of multiple variations on the same gene, + # but skip multi-gene profiles (eg fusions) + gene_names = ei.molecular_profile.variants.map do |variant| + feature_instance = variant.feature.feature_instance + if feature_instance.__typename == "Gene" + feature_instance.name.upcase + else # skip Factors for now + nil + end + end.compact.uniq + next if gene_names.length != 1 + gc = GeneClaim.joins(:source).where(sources: {source_db_name: "CIViC"}, gene_claims: {name: gene_names[0]}).first + + drug_claims = ei.therapies.map do |therapy| + drug_name = therapy.name.upcase + DrugClaim.joins(:source).where(sources: { source_db_name: "CIViC" }, drug_claims: { name: drug_name }).first + end + drug_claims = drug_claims.uniq.compact + next if drug_claims.length == 0 + + create_gene_claim_category(gc, 'DRUG RESISTANCE') if ei.significance.downcase == 'resistance' + create_gene_claim_category(gc, 'CLINICALLY ACTIONABLE') if ei.evidence_level == 'A' + drug_claims.each do |dc| + create_entries_for_evidence_item(ei, dc, gc) + end + end + backfill_publication_information + end + def create_new_source @source ||= Source.create( { @@ -38,62 +119,6 @@ def create_new_source @source.source_types << SourceType.find_by(type: 'potentially_druggable') @source.save end - - def importable_eid?(evidence_item) - [ - evidence_item.evidence_type == 'PREDICTIVE', - evidence_item.evidence_direction == 'SUPPORTS', - evidence_item.evidence_level != 'E', - evidence_item.evidence_rating.present? && evidence_item.evidence_rating > 2 - ].all? - end - - def importable_drug?(drug) - drug.name.upcase != 'N/A' && !drug.name.include?(';') - end - - def create_gene_claim_entries(gene) - gc = create_gene_claim(gene.name) - base_aliases = gene.gene_aliases + [gene.official_name] - base_aliases.uniq.reject { |n| n == gene.name }.each do |gene_alias| - create_gene_claim_alias(gc, gene_alias, GeneNomenclature::SYMBOL) - end - create_gene_claim_alias(gc, "ncbigene:#{gene.entrez_id}", GeneNomenclature::NCBI_ID) - create_gene_claim_alias(gc, "civic.gid:#{gene.id}", GeneNomenclature::CIVIC_ID) - gc - end - - def create_entries_for_evidence_item(gc, ei) - ei.therapies.select { |d| importable_drug?(d) }.each do |drug| - create_gene_claim_category(gc, 'DRUG RESISTANCE') if ei.significance.downcase == 'resistance' - create_gene_claim_category(gc, 'CLINICALLY ACTIONABLE') if ei.evidence_level == 'A' - - dc = create_drug_claim(drug.name.upcase, DrugNomenclature::PRIMARY_NAME) - create_drug_claim_alias(dc, "ncit:#{drug.ncit_id}", DrugNomenclature::NCIT_ID) if drug.ncit_id - - ic = create_interaction_claim(gc, dc) - if ei.source.citation_id.present? && ei.source.source_type == 'PubMed' - create_interaction_claim_publication(ic, ei.source.citation_id) - end - create_interaction_claim_link(ic, ei.name, "https://civicdb.org/#{ei.link}") - end - end - - def create_interaction_claims - api_client = ApiClient.new - api_client.enumerate_variants.each do |variant_edge| - mp_nodes = variant_edge.node.molecular_profiles.nodes.select { |mp| mp.variants.size == 1 } - ei_nodes = mp_nodes.reduce([]) { |tot, node| tot.concat(node.evidence_items.nodes) } - ei_nodes = ei_nodes.select { |ei| importable_eid?(ei) } - next if ei_nodes.length.zero? - - gc = create_gene_claim_entries(variant_edge.node.gene) - ei_nodes.each do |ei_node| - create_entries_for_evidence_item(gc, ei_node) - end - end - backfill_publication_information - end end end end diff --git a/server/lib/genome/importers/file_importers/drugbank.rb b/server/lib/genome/importers/file_importers/drugbank.rb index 7547848a..a200c0c3 100644 --- a/server/lib/genome/importers/file_importers/drugbank.rb +++ b/server/lib/genome/importers/file_importers/drugbank.rb @@ -26,12 +26,12 @@ def create_new_source { base_url: 'https://go.drugbank.com/drugs', site_url: 'https://go.drugbank.com/', - citation: 'Wishart DS, Feunang YD, Guo AC, Lo EJ, Marcu A, Grant JR, Sajed T, Johnson D, Li C, Sayeeda Z, Assempour N, Iynkkaran I, Liu Y, Maciejewski A, Gale N, Wilson A, Chin L, Cummings R, Le D, Pon A, Knox C, Wilson M. DrugBank 5.0: a major update to the DrugBank database for 2018. Nucleic Acids Res. 2018 Jan 4;46(D1):D1074-D1082. doi: 10.1093/nar/gkx1037. PMID: 29126136; PMCID: PMC5753335.', - citation_short: 'Wishart DS, et al. DrugBank 5.0: a major update to the DrugBank database for 2018. Nucleic Acids Res. 2018 Jan 4;46(D1):D1074-D1082.', - pmid: '29126136', - pmcid: 'PMC5753335', - doi: '10.1093/nar/gkx1037', - source_db_version: '5.1.10', + citation: 'Knox C, Wilson M, Klinger CM, Franklin M, Oler E, Wilson A, Pon A, Cox J, Chin NEL, Strawbridge SA, Garcia-Patino M, Kruger R, Sivakumaran A, Sanford S, Doshi R, Khetarpal N, Fatokun O, Doucet D, Zubkowski A, Rayat DY, Jackson H, Harford K, Anjum A, Zakir M, Wang F, Tian S, Lee B, Liigand J, Peters H, Wang RQR, Nguyen T, So D, Sharp M, da Silva R, Gabriel C, Scantlebury J, Jasinski M, Ackerman D, Jewison T, Sajed T, Gautam V, Wishart DS. DrugBank 6.0: the DrugBank Knowledgebase for 2024. Nucleic Acids Res. 2024 Jan 5;52(D1):D1265-D1275. doi: 10.1093/nar/gkad976. PMID: 37953279; PMCID: PMC10767804.', + citation_short: 'Knox C, et al. DrugBank 6.0: the DrugBank Knowledgebase for 2024. Nucleic Acids Res. 2024 Jan 5;52(D1):D1265-D1275.', + pmid: '37953279', + pmcid: 'PMC10767804', + doi: '10.1093/nar/gkad976', + source_db_version: '5.1.12', source_db_name: 'DrugBank', full_name: 'DrugBank - Open Data Drug & Drug Target Database', license: License::CUSTOM_NON_COMMERCIAL, diff --git a/server/lib/genome/importers/file_importers/pharmgkb.rb b/server/lib/genome/importers/file_importers/pharmgkb.rb index 338855ad..88dbfdec 100644 --- a/server/lib/genome/importers/file_importers/pharmgkb.rb +++ b/server/lib/genome/importers/file_importers/pharmgkb.rb @@ -23,7 +23,7 @@ def create_new_source pmid: '34216021', pmcid: 'PMC8457105', doi: '10.1002/cpt.2350', - source_db_version: '2020-08-18', # using static file, see issue #420 + source_db_version: '2024-04-05', # using static file, see issue #420 source_db_name: source_db_name, full_name: 'PharmGKB - The Pharmacogenomics Knowledgebase', license: License::CC_BY_SA_4_0, diff --git a/server/lib/genome/names.rb b/server/lib/genome/names.rb index e0096a1b..e82513f5 100644 --- a/server/lib/genome/names.rb +++ b/server/lib/genome/names.rb @@ -44,6 +44,7 @@ module DrugNomenclature NCIT_ID = 'NCIt ID' PFAM_ID = 'PFAM ID' TTD_ID = 'TTD ID' + CIVIC_TID = "CIViC Therapy ID" end module DrugAttributeName @@ -75,7 +76,7 @@ module GeneNomenclature UNIPROTKB_NAME = 'UniProtKB Entry Name' UNIPROTKB_PROTEIN_NAME = 'UniProtKB Protein Name' UNIPROTKB_GENE_NAME = 'UniProtKB Gene Name' - CIVIC_ID = 'CIViC ID' + CIVIC_GID = 'CIViC Gene ID' TTD_ID = 'TTD ID' PHARMGKB_ID = 'PharmGKB ID' CHEMBL_ID = 'ChEMBL ID' diff --git a/server/spec/factories/interaction.rb b/server/spec/factories/interaction.rb index 424f01d9..88910bc6 100644 --- a/server/spec/factories/interaction.rb +++ b/server/spec/factories/interaction.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :interaction do - sequence(:id) { |i| "DRUG #{i}" } # should always be uppercase + sequence(:id) { |i| "INTERACTION #{i}" } # should always be uppercase gene drug score { rand } diff --git a/server/spec/queries/interactions_queries_spec.rb b/server/spec/queries/interactions_queries_spec.rb new file mode 100644 index 00000000..338914b3 --- /dev/null +++ b/server/spec/queries/interactions_queries_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +RSpec.describe 'Interactions queries', type: :graphql do + before(:example) do + @drug1 = create(:drug) + @drug2 = create(:drug) + @drug3 = create(:drug) + + @gene1 = create(:gene) + @gene2 = create(:gene) + + @int1 = create(:interaction, drug: @drug1, gene: @gene1) + @int2 = create(:interaction, drug: @drug1, gene: @gene2) + @int3 = create(:interaction, drug: @drug2, gene: @gene1) + @int4 = create(:interaction, drug: @drug3, gene: @gene1) + end + + let :interactions_name_query do + <<-GRAPHQL + query interactions($drugNames: [String!], $geneNames: [String!]) { + interactions(drugNames: $drugNames, geneNames: $geneNames) { + edges { + node { + id + } + } + } + } + GRAPHQL + end + + it 'should execute basic interactions searches by name correctly' do + result = execute_graphql(interactions_name_query, variables: { drugNames: [@drug1.name] }) + expect(result["data"]["interactions"]["edges"].length).to eq 2 + + result = execute_graphql(interactions_name_query, variables: { drugNames: [@drug1.name], geneNames: [@gene1.name]}) + expect(result["data"]["interactions"]["edges"].length).to eq 1 + + result = execute_graphql(interactions_name_query, variables: { drugNames: [@drug1.name] , geneNames: []}) + expect(result["data"]["interactions"]["edges"].length).to eq 2 + + result = execute_graphql(interactions_name_query, variables: { drugNames: [], geneNames: [@gene1.name] }) + expect(result["data"]["interactions"]["edges"].length).to eq 3 + + result = execute_graphql(interactions_name_query, variables: { drugNames: [], geneNames: [@gene2.name] }) + expect(result["data"]["interactions"]["edges"].length).to eq 1 + + result = execute_graphql(interactions_name_query, variables: { drugNames: [@drug1.name, @drug2.name, @drug3.name], geneNames: [@gene1.name, @gene2.name] }) + expect(result["data"]["interactions"]["edges"].length).to eq 4 + end +end