From ba8f1c49c520f54818966bf38920470dadb69d6b Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Wed, 17 Mar 2021 00:54:19 -0400 Subject: [PATCH 01/19] list queries, count queries, facet queries, facet constraint queries, and sorting, all using the same query building engine, using pharos_config tables to find the data --- package.json | 1 + src/TCRD.js | 5 +- src/__tests__/allFacetQueries.test.js | 88 +++++ src/__tests__/getFacetQuery.test.js | 247 +++++++++++++ src/__tests__/getListQuery.test.js | 108 ++++++ .../{graphql.test.js => graphql.test.js.dont} | 4 +- src/__tests__/listAndFacetQueries.js | 279 +++++++++++++++ src/__tests__/queryDefinition.test.js | 74 ++++ src/__utils/loadTCRDforTesting.js | 27 ++ src/index.js | 2 +- src/models/DataModelList.ts | 244 ++++++------- src/models/FacetFactory.ts | 40 +-- src/models/FacetInfo.ts | 160 --------- src/models/FieldInfo.ts | 256 ++++++++++++++ src/models/IBuildable.ts | 15 + src/models/config.ts | 330 ++---------------- src/models/databaseConfig.ts | 266 ++++++++------ src/models/databaseTable.ts | 83 +---- src/models/disease/diseaseFacetFactory.ts | 26 +- src/models/disease/diseaseList.ts | 40 +-- src/models/ligand/ligandFacetFactory.ts | 35 +- src/models/ligand/ligandList.ts | 28 +- src/models/queryDefinition.ts | 170 +++++++++ src/models/similarTargets/jaccard.ts | 16 +- src/models/sqlTable.ts | 35 ++ src/models/target/targetDetails.ts | 8 +- src/models/target/targetFacetFactory.ts | 84 +---- src/models/target/targetList.ts | 240 +++++-------- src/resolvers.js | 30 +- 29 files changed, 1782 insertions(+), 1159 deletions(-) create mode 100644 src/__tests__/allFacetQueries.test.js create mode 100644 src/__tests__/getFacetQuery.test.js create mode 100644 src/__tests__/getListQuery.test.js rename src/__tests__/{graphql.test.js => graphql.test.js.dont} (99%) create mode 100644 src/__tests__/listAndFacetQueries.js create mode 100644 src/__tests__/queryDefinition.test.js create mode 100644 src/__utils/loadTCRDforTesting.js delete mode 100644 src/models/FacetInfo.ts create mode 100644 src/models/FieldInfo.ts create mode 100644 src/models/IBuildable.ts create mode 100644 src/models/queryDefinition.ts create mode 100644 src/models/sqlTable.ts diff --git a/package.json b/package.json index b7de194..97f8533 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "src/index.js", "scripts": { "test" : "node src/index.js & (sleep 10 && jest)", + "justTests": "jest", "start" : "nodemon src/index.js", "start:ci" : "tsc && node src/index.js", "gcp-build" : "tsc -p ." diff --git a/src/TCRD.js b/src/TCRD.js index 500a510..fd06894 100644 --- a/src/TCRD.js +++ b/src/TCRD.js @@ -56,7 +56,7 @@ function validRegex(pattern) { try { var re = new RegExp(pattern, 'i'); match = re.test('test pattern doesnt matter'); - } catch { + } catch (e) { return false; } return true; @@ -890,8 +890,7 @@ and NOT (a.ppitypes = 'STRINGDB' AND a.score < ${confidence}) and b1.target_id = ?`, [target.tcrdid])); if (sort) { - q = q.orderBy([{column: 'a.p_int', order: 'desc'}, {column: 'a.score', order: 'desc'}, - ]); + q = q.orderBy([{column: 'a.p_int', order: 'desc'}, {column: 'a.score', order: 'desc'}]); } if (args.top) { diff --git a/src/__tests__/allFacetQueries.test.js b/src/__tests__/allFacetQueries.test.js new file mode 100644 index 0000000..82543f3 --- /dev/null +++ b/src/__tests__/allFacetQueries.test.js @@ -0,0 +1,88 @@ +const {TargetList} = require("../models/target/targetList"); +const {tcrdConfig} = require('../__utils/loadTCRDforTesting'); +const TCRD = require('../TCRD'); + +let tcrd = new TCRD(tcrdConfig); + +let allTargets; +let filteredTargets; + +beforeAll(() => { + return tcrd.tableInfo.loadPromise.then(() => { + allTargets = new TargetList(tcrd, {facets: "all"}); + filteredTargets = new TargetList(tcrd, { + facets: "all", filter: { + facets: [ + {facet: "Family", values: ["Kinase"]}, + {facet: "Target Development Level", values: ["Tclin"]} + ] + } + }); + }) +}); + +describe('all facets should work', function () { + + test('there should be facets', () => { + expect(allTargets.facetsToFetch.length).toBeGreaterThan(35); + }); + + [ + "Target Development Level", + "IDG Target Lists", + "UniProt Keyword", + "Family", + "Indication", + "Monarch Disease", + "UniProt Disease", + "Ortholog", + // "IMPC Phenotype", + "GO Component", + "GO Process", + "JAX/MGI Phenotype", + "GO Function", + "GWAS", + "Expression: HPA", + "Expression: JensenLab Experiment HPA", + "Expression: HPM Gene", + "Expression: JensenLab Experiment Exon array", + "Expression: Consensus", + "Expression: JensenLab Experiment GNF", + "Expression: JensenLab Experiment HPA-RNA", + "Expression: JensenLab Experiment RNA-seq", + "Expression: JensenLab Experiment UniGene", + "Expression: UniProt Tissue", + "Expression: JensenLab Experiment Cardiac proteome", + "Expression: JensenLab Text Mining", + "Expression: JensenLab Knowledge UniProtKB-RC", + "Expression: Cell Surface Protein Atlas", + "Reactome Pathway", + "WikiPathways Pathway", + "KEGG Pathway", + "Interacting Viral Protein", + "Interacting Virus", + "Log Novelty", + "Log PubMed Score", + "Data Source", + "UniProt Pathway", + "PathwayCommons Pathway", + "PANTHER Class", + "DTO Class" + ].forEach(facetType => { + test(`${facetType} should work`, () => { + const fullFacet = allTargets.facetsToFetch.find(f => f.name == facetType); + const filteredFacet = filteredTargets.facetsToFetch.find(f => f.name == facetType); + return Promise.all([fullFacet.getFacetQuery(), filteredFacet.getFacetQuery()]).then(res => { + const fullCounts = res[0]; + const filteredCounts = res[1]; + + const fullTotal = fullCounts.reduce((a, c) => a + c.value, 0); + const filteredTotal = filteredCounts.reduce((a, c) => a + c.value, 0); + + expect(fullTotal).toBeGreaterThan(0); + expect(filteredTotal).toBeGreaterThan(0); + expect(fullTotal).toBeGreaterThan(filteredTotal); + }); + }); + }); +}); diff --git a/src/__tests__/getFacetQuery.test.js b/src/__tests__/getFacetQuery.test.js new file mode 100644 index 0000000..302cc9a --- /dev/null +++ b/src/__tests__/getFacetQuery.test.js @@ -0,0 +1,247 @@ +const {TargetList} = require("../models/target/targetList"); +const {tcrdConfig} = require('../__utils/loadTCRDforTesting'); +const TCRD = require('../TCRD'); + +let tcrd = new TCRD(tcrdConfig); + +beforeAll(() => { + return tcrd.tableInfo.loadPromise.then((res) => { + return; + }); +}); + +describe('list queries should work', function () { + test('TDL facet looks right', () => { + const listObj = new TargetList(tcrd, {}); + + const facet = listObj.facetsToFetch.find(facet => facet.name === "Target Development Level"); + const query = facet.getFacetQuery(); + return query.then(res => { + expect(res.length).toBe(4); + res.forEach(row => { + expect(row.name).toBeTruthy(); + expect(row.value).toBeGreaterThanOrEqual(0); + }); + }); + }); + + + test('Facet with custom select looks right', () => { + const listObj = new TargetList(tcrd, {facets: ["Family"]}); + const query = listObj.facetsToFetch.find(facet => facet.name === "Family").getFacetQuery(); + return query.then(res => { + const TFrow = res.find(row => row.name == 'Transcription Factor'); // translated from TF + expect(TFrow.name).toBe('Transcription Factor'); + res.slice(0, 5).forEach(row => { + expect(row.name).toBeTruthy(); + expect(row.value).toBeGreaterThanOrEqual(0); + }); + }); + }); + + test('facet constraints are constraining something', () => { + const listObj = new TargetList(tcrd, {filter: {facets: [{facet: "Family", values: ["Kinase"]}]}}); + const tdlFacet = listObj.facetsToFetch.find(facet => facet.name === "Target Development Level").getFacetQuery(); + const famFacet = listObj.facetsToFetch.find(facet => facet.name === "Family").getFacetQuery(); + return Promise.all([tdlFacet, famFacet]).then(res => { + const tdlData = res[0]; + const famData = res[1]; + const tdlCount = tdlData.reduce((a, c) => a + c.value, 0); + const famCount = famData.reduce((a, c) => a + c.value, 0); + + expect(tdlCount).toBeLessThan(famCount); // b/c TDL is filtered by Fam facet, and Fam facet doesn't filter itself + }); + }); + + test('numeric facets work too!', () => { + const listObj = new TargetList(tcrd, {facets: ["Log Novelty"]}); + const noveltyFacet = listObj.facetsToFetch.find(facet => facet.name === "Log Novelty"); + return noveltyFacet.getFacetQuery().then(res => { + res.forEach(row => { + expect(row.bin).toBe(+row.bin); + expect(row.value % noveltyFacet.binSize).toBe(0); + expect(row.value).toBeGreaterThanOrEqual(0); + }); + }); + }); + + test('precalculated facets work too!', () => { + const listObj = new TargetList(tcrd, {facets: ["IMPC Phenotype"]}); + const impc = listObj.facetsToFetch.find(facet => facet.name === "IMPC Phenotype"); + return impc.getFacetQuery().then(res => { + expect(res.length).toBeGreaterThan(700); + res.slice(0, 5).forEach(row => { + expect(row.name).toBeTruthy(); + expect(row.value).toBeGreaterThanOrEqual(0); + }); + }); + }); + + test('precalculated facets with where clauses work too!', () => { + const listObj = new TargetList(tcrd, {facets: ["Expression: HPA"]}); + const hpa = listObj.facetsToFetch.find(facet => facet.name === "Expression: HPA"); + return hpa.getFacetQuery().then(res => { + expect(res.length).toBeGreaterThan(100); + res.slice(0, 5).forEach(row => { + expect(row.name).toBeTruthy(); + expect(row.value).toBeGreaterThanOrEqual(0); + }); + }); + }); + + test('Facet constraint queries are people too!', () => { + const listObj = new TargetList(tcrd, { + filter: { + facets: [{ + facet: "Family", + values: ["Kinase", "Transcription Factor"] + }] + } + }); + const famFacet = listObj.facetsToFetch.find(facet => facet.name === "Family"); + return Promise.all([famFacet.getFacetQuery(), famFacet.getFacetConstraintQuery()]).then(res => { + const famCounts = res[0]; + const proteinList = res[1]; + + const kinaseCount = famCounts.find(row => row.name == 'Kinase').value; + const tfCount = famCounts.find(row => row.name == 'Transcription Factor').value; + + expect(proteinList.length).toBe(kinaseCount + tfCount); + + proteinList.slice(0, 5).forEach(row => { + expect(row.id).toBeGreaterThan(0); + }); + }); + }); + + test('Facet constraint sometimes have where clauses', () => { + const listObj = new TargetList(tcrd, {filter: {facets: [{facet: "UniProt Keyword", values: ["Lyase"]}]}}); + const upFacet = listObj.facetsToFetch.find(facet => facet.name === "UniProt Keyword"); + + return Promise.all([upFacet.getFacetQuery(), upFacet.getFacetConstraintQuery()]).then(res => { + const upCount = res[0]; + const proteinList = res[1]; + const lyaseCount = upCount.find(row => row.name == 'Lyase').value; + + expect(proteinList.length).toBe(lyaseCount); + + proteinList.slice(0, 5).forEach(row => { + expect(row.id).toBeGreaterThan(0); + }); + }); + }); + + test('Virus queries are linky and clausy', () => { + const listObj = new TargetList(tcrd, { + filter: { + facets: [{ + facet: "Interacting Virus", + values: ["Horsepox virus"] + }] + } + }); + const virusFacet = listObj.facetsToFetch.find(facet => facet.name === "Interacting Virus"); + + return Promise.all([virusFacet.getFacetQuery(), virusFacet.getFacetConstraintQuery()]).then(res => { + const countResults = res[0]; + const proteinList = res[1]; + const oneCount = countResults.find(row => row.name == 'Horsepox virus').value; + expect(proteinList.length).toBe(oneCount); + proteinList.slice(0, 5).forEach(row => { + expect(row.id).toBeGreaterThan(0); + }); + }); + }); + + test('filtering by numeric facets!', () => { + const listObj = new TargetList(tcrd, {filter: {facets: [{facet: "Log Novelty", values: ["[ -5.5, -3.5 )"]}]}}); + const facet = listObj.facetsToFetch.find(facet => facet.name === "Log Novelty"); + + return Promise.all([facet.getFacetQuery(), facet.getFacetConstraintQuery()]).then(res => { + const countResults = res[0]; + const proteinList = res[1]; + + const rangeCount = countResults.filter(row => row.bin >= -5.5 && row.bin < -3.5).reduce((a, c) => a + c.value, 0); + expect(proteinList.length).toBe(rangeCount); + proteinList.slice(0, 5).forEach(row => { + expect(row.id).toBeGreaterThan(0); + }); + }); + }); + + + test('valuesDelimited boo!', () => { + const listObj = new TargetList(tcrd, { + filter: { + associatedTarget: "ACE2", + facets: [{facet: "PPI Data Source", values: ["STRINGDB"]}] + } + }); + const facet = listObj.facetsToFetch.find(facet => facet.name === "PPI Data Source"); + + console.log(facet.getFacetQuery().toString()); + console.log(facet.getFacetConstraintQuery().toString()); + + return Promise.all([facet.getFacetQuery(), facet.getFacetConstraintQuery()]).then(res => { + const countResults = res[0]; + const proteinList = res[1]; + + const oneCount = countResults.filter(row => row.name && row.name.includes('STRINGDB')).reduce((a, c) => a + c.value, 0); + expect(proteinList.length).toBe(oneCount); + proteinList.slice(0, 5).forEach(row => { + expect(row.id).toBeGreaterThan(0); + }); + }); + }); + + + test('facets with an interacting Target!', () => { + const ppiList = new TargetList(tcrd, { + filter: { + associatedTarget: "ACE2" + } + }); + const fullList = new TargetList(tcrd, { }); + + const ppiFacet = ppiList.facetsToFetch.find(facet => facet.name === "Target Development Level"); + const fullFacet = fullList.facetsToFetch.find(facet => facet.name === "Target Development Level"); + + console.log(ppiFacet.getFacetQuery().toString()); + console.log(fullFacet.getFacetQuery().toString()); + + return Promise.all([ppiFacet.getFacetQuery(), fullFacet.getFacetQuery()]).then(res => { + console.log(res); + + const ppiResults = res[0]; + const fullResults = res[1]; + + const ppiCount = ppiResults.reduce((a, c) => a + c.value, 0); + const fullCount = fullResults.reduce((a, c) => a + c.value, 0); + + expect(fullCount).toBeGreaterThan(ppiCount); + }); + }); + + test('filtering by numeric facets, and an associated target!', () => { + const ppiList = new TargetList(tcrd, {filter: {associatedTarget: "ACE2", facets: [{facet: "Log Novelty", values: ["[ -5.5, -3.5 )"]}]}}); + const ppiFacet = ppiList.facetsToFetch.find(facet => facet.name === "Target Development Level"); + const fullList = new TargetList(tcrd, {filter: {facets: [{facet: "Log Novelty", values: ["[ -5.5, -3.5 )"]}]}}); + const fullFacet = fullList.facetsToFetch.find(facet => facet.name === "Target Development Level"); + + console.log(ppiFacet.getFacetQuery().toString()); + console.log(fullFacet.getFacetQuery().toString()); + + return Promise.all([ppiFacet.getFacetQuery(), fullFacet.getFacetQuery()]).then(res => { + console.log(res); + const ppiResults = res[0]; + const fullResults = res[1]; + + const ppiCount = ppiResults.reduce((a, c) => a + c.value, 0); + const fullCount = fullResults.reduce((a, c) => a + c.value, 0); + + expect(fullCount).toBeGreaterThan(ppiCount); + + }); + }); + +}); diff --git a/src/__tests__/getListQuery.test.js b/src/__tests__/getListQuery.test.js new file mode 100644 index 0000000..f43a61f --- /dev/null +++ b/src/__tests__/getListQuery.test.js @@ -0,0 +1,108 @@ +const {TargetList} = require("../models/target/targetList"); +const {tcrdConfig} = require('../__utils/loadTCRDforTesting'); +const TCRD = require('../TCRD'); + +let tcrd = new TCRD(tcrdConfig); + +beforeAll(() => { + return tcrd.tableInfo.loadPromise.then((res) => { + return; + }); +}); + +describe('list queries should work', function () { + test('protein - target linking', () => { + const listObj = new TargetList(tcrd, { + filter: {facets: [{facet: "Family", values: ["Kinase", "Transcription Factor"]}]}, + top: 10, + fields: ["Target Development Level", "Family"] + }); + // console.log(listObj.getListQuery().toString()); + return listObj.getListQuery().then(res => { + expect(res.length).toBe(10); + expect(res[0]["Target Development Level"]).toBeTruthy(); + expect(res[0]["Family"]).toBeTruthy(); + }); + }); + test('download query should work with an extra join constraint', () => { + const listObj = new TargetList(tcrd, { + filter: {facets: [{facet: "Family", values: ["Kinase", "Transcription Factor"]}]}, + top: 10, + fields: ["UniProt Keyword"] + }); + // console.log(listObj.getListQuery().toString()); + return listObj.getListQuery().then(res => { + expect(res.length).toBe(10); + expect(res[0]["UniProt Keyword"]).toBeTruthy(); + }); + }); + test('download query should work with duplicated tables', () => { + const listObj = new TargetList(tcrd, { + filter: {facets: [{facet: "Data Source", values: ["RCSB Protein Data Bank"]}]}, + top: 10, + fields: ["UniProt Keyword", "PDB ID"] + }); + // console.log(listObj.getListQuery().toString()); + return listObj.getListQuery().then(res => { + expect(res.length).toBe(10); + expect(res[0]["UniProt Keyword"]).toBeTruthy(); + expect(res[0]["PDB ID"]).toBeTruthy(); + }); + }); + test('group method = count groups and counts', () => { + const listObj = new TargetList(tcrd, { + filter: {facets: [{facet: "Family", values: ["Kinase", "Transcription Factor"]}]}, + top: 10, + fields: ["Family", "UniProt", "PDB Count"] + }); + // console.log(listObj.getListQuery().toString()); + return listObj.getListQuery().then(res => { + expect(res.length).toBe(10); + res.forEach(val => { + expect(val["UniProt"]).toBeTruthy(); + expect(val["PDB Count"]).toBeGreaterThanOrEqual(0); + }); + }); + }); + + test('list with an associated target', () => { + const listObj = new TargetList(tcrd, { + filter: { + associatedTarget: 'ACE2', + facets: [{facet: "PPI Data Source", values: ["BioPlex"]}] + }, + top: 10, + fields: [ + "UniProt", + "Symbol", + "PPI Data Source", + "StringDB Interaction Score", + "BioPlex Interaction Probability", + "BioPlex p_ni", + "BioPlex p_wrong"] + }); + // console.log(listObj.getListQuery().toString()); + return listObj.getListQuery().then(res => { + expect(res.length).toBeGreaterThan(0); + expect(res[0]["UniProt"]).toBeTruthy(); + expect(res[0]["Symbol"]).toBeTruthy(); + expect(res[0]["PPI Data Source"]).toBeTruthy(); + expect(res[0]["StringDB Interaction Score"]).toBeGreaterThan(0); + expect(res[0]["BioPlex Interaction Probability"]).toBeGreaterThan(0); + expect(res[0]["BioPlex p_ni"]).toBeGreaterThan(0); + expect(res[0]["BioPlex p_wrong"]).toBeGreaterThan(0); + }); + // expecting data like this, a new TCRD might return something with some nulls + // RowDataPacket { + // id: 16144, + // UniProt: 'Q13685', + // Symbol: 'AAMP', + // 'PPI Data Source': 'BioPlex,STRINGDB', + // 'StringDB Interaction Score': 0.519, + // 'BioPlex Interaction Probability': 0.821892143, + // 'BioPlex p_ni': 1.5e-8, + // 'BioPlex p_wrong': 0.178107842 + // } + }); + +}); diff --git a/src/__tests__/graphql.test.js b/src/__tests__/graphql.test.js.dont similarity index 99% rename from src/__tests__/graphql.test.js rename to src/__tests__/graphql.test.js.dont index dc5cd96..5953eed 100644 --- a/src/__tests__/graphql.test.js +++ b/src/__tests__/graphql.test.js.dont @@ -3,7 +3,7 @@ const request = require('sync-request'); const tester = require('graphql-tester').tester; testDirectory = './tests'; -const PORT = process.env.PORT || 4000; +const PORT = process.env.PORT || 4444; url = `http://localhost:${PORT}/graphql/`; function postit(query) { @@ -293,4 +293,4 @@ describe('GraphQL API Tests', function () { expect(obj.errors[0].message).toBe(null); } }); -}); \ No newline at end of file +}); diff --git a/src/__tests__/listAndFacetQueries.js b/src/__tests__/listAndFacetQueries.js new file mode 100644 index 0000000..fc283bc --- /dev/null +++ b/src/__tests__/listAndFacetQueries.js @@ -0,0 +1,279 @@ +const {TargetList} = require("../models/target/targetList"); +const {tcrdConfig} = require('../__utils/loadTCRDforTesting'); +const TCRD = require('../TCRD'); + +let tcrd = new TCRD(tcrdConfig); + +beforeAll(() => { + return tcrd.tableInfo.loadPromise.then((res) => { + return; + }); +}); + +describe('all the queries should be consistent with each other', function () { + test('All targets query', () => { + const fullList = new TargetList(tcrd, {top:1000000}); + const filteredList = new TargetList(tcrd, {filter: {facets: [{facet: "Target Development Level", values: ["Tclin"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullFamFacet = fullList.facetsToFetch.find(facet => facet.name === 'Family'); + const fullFamFacetQuery = fullFamFacet.getFacetQuery(); + const fullTDLFacet = fullList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const fullTDLFacetQuery = fullTDLFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredFamFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Family'); + const filteredFamFacetQuery = filteredFamFacet.getFacetQuery(); + const filteredTDLFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const filteredTDLFacetQuery = filteredTDLFacet.getFacetQuery(); + const filteredTDLConstraintQuery = filteredTDLFacet.getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullFamFacetQuery, fullTDLFacetQuery, + filteredCountQuery, filteredListQuery, filteredFamFacetQuery, filteredTDLFacetQuery, + filteredTDLConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullFamCount = res[2].reduce((a, c) => a + c.value, 0); + const fullTDLCount = res[3].reduce((a, c) => a + c.value, 0); + expect(fullFamCount).toBe(fullListLength); + expect(fullTDLCount).toBe(fullListLength); + + const filteredCount = res[4][0].count; + const filteredListLength = res[5].length; + expect(filteredCount).toBe(filteredListLength); + const filteredFamCount = res[6].reduce((a, c) => a + c.value, 0); + const filteredTDLCount = res[7].find(row => row.name == 'Tclin').value; // facets don't filter themselves + expect(filteredFamCount).toBe(filteredListLength); + expect(filteredTDLCount).toBe(filteredListLength); + + const facetConstraintList = res[8].length; + + expect(facetConstraintList).toBe(filteredListLength); + expect(fullCount).toBeGreaterThan(filteredCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + + test('Search term query', () => { + const fullList = new TargetList(tcrd, {top:1000000, filter: {term:'cdk'}}); + const filteredList = new TargetList(tcrd, {filter: {term:'cdk', facets: [{facet: "Target Development Level", values: ["Tclin"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullFamFacet = fullList.facetsToFetch.find(facet => facet.name === 'Family'); + const fullFamFacetQuery = fullFamFacet.getFacetQuery(); + const fullTDLFacet = fullList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const fullTDLFacetQuery = fullTDLFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredFamFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Family'); + const filteredFamFacetQuery = filteredFamFacet.getFacetQuery(); + const filteredTDLFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const filteredTDLFacetQuery = filteredTDLFacet.getFacetQuery(); + const filteredTDLConstraintQuery = filteredTDLFacet.getFacetConstraintQuery(); + + + return Promise.all( + [fullCountQuery, fullListQuery, fullFamFacetQuery, fullTDLFacetQuery, + filteredCountQuery, filteredListQuery, filteredFamFacetQuery, filteredTDLFacetQuery, + filteredTDLConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullFamCount = res[2].reduce((a, c) => a + c.value, 0); + const fullTDLCount = res[3].reduce((a, c) => a + c.value, 0); + expect(fullFamCount).toBe(fullListLength); + expect(fullTDLCount).toBe(fullListLength); + + const filteredCount = res[4][0].count; + const filteredListLength = res[5].length; + expect(filteredCount).toBe(filteredListLength); + const filteredFamCount = res[6].reduce((a, c) => a + c.value, 0); + const filteredTDLCount = res[7].find(row => row.name == 'Tclin').value; // facets don't filter themselves + expect(filteredFamCount).toBe(filteredListLength); + expect(filteredTDLCount).toBe(filteredListLength); + + const facetConstraintList = res[8].length; + + // expect(facetConstraintList).toBe(filteredListLength); // not true anymore because the facets are only constraining their one thing + expect(fullCount).toBeGreaterThan(filteredCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + + test('PPI query', () => { + const fullList = new TargetList(tcrd, {top:1000000, filter: {associatedTarget: "ACE2"}}); + const filteredList = new TargetList(tcrd, {filter: {associatedTarget: "ACE2", facets: [{facet: "Target Development Level", values: ["Tclin"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullFamFacet = fullList.facetsToFetch.find(facet => facet.name === 'Family'); + const fullFamFacetQuery = fullFamFacet.getFacetQuery(); + const fullTDLFacet = fullList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const fullTDLFacetQuery = fullTDLFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredFamFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Family'); + const filteredFamFacetQuery = filteredFamFacet.getFacetQuery(); + const filteredTDLFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const filteredTDLFacetQuery = filteredTDLFacet.getFacetQuery(); + const filteredTDLConstraintQuery = filteredTDLFacet.getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullFamFacetQuery, fullTDLFacetQuery, + filteredCountQuery, filteredListQuery, filteredFamFacetQuery, filteredTDLFacetQuery, + filteredTDLConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullFamCount = res[2].reduce((a, c) => a + c.value, 0); + const fullTDLCount = res[3].reduce((a, c) => a + c.value, 0); + expect(fullFamCount).toBe(fullListLength); + expect(fullTDLCount).toBe(fullListLength); + + const filteredCount = res[4][0].count; + const filteredListLength = res[5].length; + expect(filteredCount).toBe(filteredListLength); + const filteredFamCount = res[6].reduce((a, c) => a + c.value, 0); + const filteredTDLCount = res[7].find(row => row.name == 'Tclin').value; // facets don't filter themselves + expect(filteredFamCount).toBe(filteredListLength); + expect(filteredTDLCount).toBe(filteredListLength); + + const facetConstraintList = res[8].length; + + // expect(facetConstraintList).toBe(filteredListLength); // not true anymore because the facets are only constraining their one thing + expect(fullCount).toBeGreaterThan(filteredCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + + test('Associated Disease query', () => { + const fullList = new TargetList(tcrd, {top:1000000, filter: {associatedDisease:"carcinoma"}}); + const filteredList = new TargetList(tcrd, {filter: {associatedDisease:"carcinoma", facets: [{facet: "Target Development Level", values: ["Tclin"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullFamFacet = fullList.facetsToFetch.find(facet => facet.name === 'Family'); + const fullFamFacetQuery = fullFamFacet.getFacetQuery(); + const fullTDLFacet = fullList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const fullTDLFacetQuery = fullTDLFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredFamFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Family'); + const filteredFamFacetQuery = filteredFamFacet.getFacetQuery(); + const filteredTDLFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const filteredTDLFacetQuery = filteredTDLFacet.getFacetQuery(); + const filteredTDLConstraintQuery = filteredTDLFacet.getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullFamFacetQuery, fullTDLFacetQuery, + filteredCountQuery, filteredListQuery, filteredFamFacetQuery, filteredTDLFacetQuery, + filteredTDLConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullFamCount = res[2].reduce((a, c) => a + c.value, 0); + const fullTDLCount = res[3].reduce((a, c) => a + c.value, 0); + expect(fullFamCount).toBe(fullListLength); + expect(fullTDLCount).toBe(fullListLength); + + const filteredCount = res[4][0].count; + const filteredListLength = res[5].length; + expect(filteredCount).toBe(filteredListLength); + const filteredFamCount = res[6].reduce((a, c) => a + c.value, 0); + const filteredTDLCount = res[7].find(row => row.name == 'Tclin').value; // facets don't filter themselves + expect(filteredFamCount).toBe(filteredListLength); + expect(filteredTDLCount).toBe(filteredListLength); + + const facetConstraintList = res[8].length; + + // expect(facetConstraintList).toBe(filteredListLength); // not true anymore because the facets are only constraining their one thing + expect(fullCount).toBeGreaterThan(filteredCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + + test('Similarity query', () => { + const fullList = new TargetList(tcrd, {top:1000000, filter: {similarity: "(Q6P1J9, GWAS)"}}); + const filteredList = new TargetList(tcrd, {filter: {similarity: "(Q6P1J9, GWAS)", facets: [{facet: "Target Development Level", values: ["Tclin"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullFamFacet = fullList.facetsToFetch.find(facet => facet.name === 'Family'); + const fullFamFacetQuery = fullFamFacet.getFacetQuery(); + const fullTDLFacet = fullList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const fullTDLFacetQuery = fullTDLFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredFamFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Family'); + const filteredFamFacetQuery = filteredFamFacet.getFacetQuery(); + const filteredTDLFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const filteredTDLFacetQuery = filteredTDLFacet.getFacetQuery(); + const filteredTDLConstraintQuery = filteredTDLFacet.getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullFamFacetQuery, fullTDLFacetQuery, + filteredCountQuery, filteredListQuery, filteredFamFacetQuery, filteredTDLFacetQuery, + filteredTDLConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullFamCount = res[2].reduce((a, c) => a + c.value, 0); + const fullTDLCount = res[3].reduce((a, c) => a + c.value, 0); + expect(fullFamCount).toBe(fullListLength); + expect(fullTDLCount).toBe(fullListLength); + + const filteredCount = res[4][0].count; + const filteredListLength = res[5].length; + expect(filteredCount).toBe(filteredListLength); + const filteredFamCount = res[6].reduce((a, c) => a + c.value, 0); + const filteredTDLCount = res[7].find(row => row.name == 'Tclin').value; // facets don't filter themselves + expect(filteredFamCount).toBe(filteredListLength); + expect(filteredTDLCount).toBe(filteredListLength); + + const facetConstraintList = res[8].length; + + // expect(facetConstraintList).toBe(filteredListLength); // not true anymore because the facets are only constraining their one thing + expect(fullCount).toBeGreaterThan(filteredCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); +}); diff --git a/src/__tests__/queryDefinition.test.js b/src/__tests__/queryDefinition.test.js new file mode 100644 index 0000000..bf7e7bc --- /dev/null +++ b/src/__tests__/queryDefinition.test.js @@ -0,0 +1,74 @@ +const {QueryDefinition} = require('../models/queryDefinition'); + +const mockBuildableObj = { + getSpecialModelWhereClause: function () {return '';}, + rootTable: 'protein' +}; + +describe('Query Builder Tests', function () { + test('one column should work', () => { + const qb = QueryDefinition.GenerateQueryDefinition(mockBuildableObj, [ + {table: 'target', column: 'tdl'} + ]); + expect(qb.buildable.rootTable).toBe('protein'); + expect(qb.tables.length).toBe(1); + expect(qb.tables[0].tableName).toBe('target'); + expect(qb.tables[0].columns.length).toBe(1); + expect(qb.tables[0].columns[0].column).toBe('tdl'); + }); + + test('two columns in one table', () => { + const qb = QueryDefinition.GenerateQueryDefinition(mockBuildableObj, [ + {table: 'target', column: 'tdl'}, + {table: 'target', column: 'fam'} + ]); + expect(qb.buildable.rootTable).toBe('protein'); + expect(qb.tables.length).toBe(1); + expect(qb.tables[0].tableName).toBe('target'); + expect(qb.tables[0].columns.length).toBe(2); + expect(qb.tables[0].columns[0].column).toBe('tdl'); + expect(qb.tables[0].columns[1].column).toBe('fam'); + }); + + test('two columns in two tables - different where_clause', () => { + const qb = QueryDefinition.GenerateQueryDefinition(mockBuildableObj, [ + {table: 'target', column: 'tdl'}, + {table: 'target', column: 'fam', where_clause: 'fam is not null'} + ]); + expect(qb.buildable.rootTable).toBe('protein'); + expect(qb.tables.length).toBe(2); + expect(qb.tables[0].tableName).toBe('target'); + expect(qb.tables[0].columns.length).toBe(1); + expect(qb.tables[0].columns[0].column).toBe('tdl'); + + expect(qb.tables[1].tableName).toBe('target'); + expect(qb.tables[1].columns.length).toBe(1); + expect(qb.tables[1].columns[0].column).toBe('fam'); + expect(qb.tables[1].joinConstraint).toBe('fam is not null'); + }); + + test('four columns in two tables - different where_clause', () => { + const qb = QueryDefinition.GenerateQueryDefinition(mockBuildableObj,[ + {table: 'target', column: 'tdl'}, + {table: 'target', column: 'fam', where_clause: 'fam is not null'}, + {table: 'target', column: 'name'}, + {table: 'target', column: 'name', where_clause: 'fam is not null'} + ]); + expect(qb.buildable.rootTable).toBe('protein'); + expect(qb.tables.length).toBe(2); + expect(qb.tables[0].tableName).toBe('target'); + expect(qb.tables[0].columns.length).toBe(2); + expect(qb.tables[0].columns[0].column).toBe('tdl'); + expect(qb.tables[0].columns[1].column).toBe('name'); + expect(qb.tables[0].joinConstraint).toBe(''); + + expect(qb.tables[1].tableName).toBe('target'); + expect(qb.tables[1].columns.length).toBe(2); + expect(qb.tables[1].columns[0].column).toBe('fam'); + expect(qb.tables[1].columns[1].column).toBe('name'); + expect(qb.tables[1].joinConstraint).toBe('fam is not null'); + }); + + + +}); diff --git a/src/__utils/loadTCRDforTesting.js b/src/__utils/loadTCRDforTesting.js new file mode 100644 index 0000000..16951e6 --- /dev/null +++ b/src/__utils/loadTCRDforTesting.js @@ -0,0 +1,27 @@ +const {cred} = require('../db_credentials'); + +jest.setTimeout(60000); + +const tcrdConfig = { + client: 'mysql', + connection: { + host: cred.DBHOST, + user: cred.USER, + password: cred.PWORD, + database: cred.DBNAME, + configDB: cred.CONFIGDB + }, + pool: { + min: 2, + max: 20, + createTimeoutMillis: 30000, + acquireTimeoutMillis: 30000, + idleTimeoutMillis: 30000, + reapIntervalMillis: 1000, + createRetryIntervalMillis: 100, + propagateCreateError: true // <- default is true, set to false + } +}; + + +exports.tcrdConfig = tcrdConfig; diff --git a/src/index.js b/src/index.js index bf3fd0c..0d163e1 100644 --- a/src/index.js +++ b/src/index.js @@ -77,5 +77,5 @@ setTimeout(() => { console.log('🏭 using configuration from: ' + cred.CONFIGDB); console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`) }); -}, 5000); +}); diff --git a/src/models/DataModelList.ts b/src/models/DataModelList.ts index 43aa248..683e285 100644 --- a/src/models/DataModelList.ts +++ b/src/models/DataModelList.ts @@ -1,53 +1,64 @@ import now from "performance-now"; -import {FacetInfo} from "./FacetInfo"; +import {FieldInfo} from "./FieldInfo"; import {FacetFactory} from "./FacetFactory"; -import {Config, ConfigKeys, QueryDefinition, RequestedData, SqlTable} from "./config"; import {DatabaseConfig} from "./databaseConfig"; // @ts-ignore import * as CONSTANTS from "../constants"; -import {DiseaseList} from "./disease/diseaseList"; -import {DatabaseTable} from "./databaseTable"; +import {QueryDefinition} from "./queryDefinition"; +import {IBuildable} from "./IBuildable"; -export abstract class DataModelList { +export abstract class DataModelList implements IBuildable { abstract addModelSpecificFiltering(query: any, list?: boolean): void; - abstract addLinkToRootTable(query: any, facet: FacetInfo): void; - abstract getRequiredTablesForFacet(info: FacetInfo): string[]; - abstract listQueryKey(): ConfigKeys; + abstract getDefaultFields(): FieldInfo[]; abstract defaultSortParameters(): { column: string, order: string }[]; + + getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string { + return ''; + } + + batch: string[] = []; facetFactory: FacetFactory; term: string = ""; fields: string[] = []; rootTable: string; keyColumn: string; - filteringFacets: FacetInfo[] = []; - facetsToFetch: FacetInfo[] = []; + filteringFacets: FieldInfo[] = []; + facetsToFetch: FieldInfo[] = []; associatedTarget: string = ""; associatedDisease: string = ""; - similarity: {match: string, facet: string} = {match:'', facet:''}; + similarity: { match: string, facet: string } = {match: '', facet: ''}; ppiConfidence: number = CONSTANTS.DEFAULT_PPI_CONFIDENCE; skip: number = 0; top: number = 10; + modelInfo: { name: string, table: string, column: string }; tcrd: any; database: any; databaseConfig: DatabaseConfig; - sortTable: string = ""; - sortColumn: string = ""; + sortField: string = ""; direction: string = ""; + getAssociatedModel(): string { + if (this.associatedTarget) { + return 'Target'; + } + if (this.associatedDisease) { + return 'Disease'; + } + return ''; + } + get AllFacets(): string[] { - const modelInfo = this.databaseConfig.modelList.get(this.rootTable); - const facetInfo = this.databaseConfig.fieldLists.get(`${modelInfo?.name} Facet - All`); - return facetInfo?.map(facet => facet.type) || []; + const fieldInfo = this.databaseConfig.getAvailableFields(this.modelInfo.name, 'facet'); + return fieldInfo.map((facet: FieldInfo) => facet.name) || []; } get DefaultFacets() { - const modelInfo = this.databaseConfig.modelList.get(this.rootTable); - const facetInfo = this.databaseConfig.fieldLists.get(`${modelInfo?.name} Facet - Default`); - return facetInfo?.sort((a,b) => a.order - b.order).map(a => a.type) || []; + const fieldInfo = this.databaseConfig.getDefaultFields(this.modelInfo.name, 'facet'); + return fieldInfo.map((facet: FieldInfo) => facet.name) || []; }; constructor(tcrd: any, rootTable: string, keyColumn: string, facetFactory: FacetFactory, json: any, extra?: any) { @@ -58,7 +69,12 @@ export abstract class DataModelList { this.keyColumn = keyColumn || this.databaseConfig.getPrimaryKey(this.rootTable); this.facetFactory = facetFactory; + this.modelInfo = this.databaseConfig.modelList.get(this.rootTable) || { name: '', table: '', column: '' }; + if (json) { + if (json.batch) { + this.batch = json.batch; + } if (json.skip) { this.skip = json.skip; } @@ -84,12 +100,12 @@ export abstract class DataModelList { const rawValues = json.filter.similarity.toString().split(','); let match = rawValues[0].trim(); - if(match[0] === '('){ + if (match[0] === '(') { match = match.slice(1).trim(); } let facet = rawValues[1].trim(); - if(facet[facet.length - 1] === ')'){ - facet = facet.slice(0,facet.length - 1).trim(); + if (facet[facet.length - 1] === ')') { + facet = facet.slice(0, facet.length - 1).trim(); } this.similarity = {match: match, facet: facet}; } @@ -97,11 +113,11 @@ export abstract class DataModelList { this.ppiConfidence = json.filter.ppiConfidence; } if (json.filter.order) { - this.sortColumn = json.filter.order.substring(1); - if (this.sortColumn.indexOf('.') > 0) { - this.sortTable = this.sortColumn.split('.')[0]; - this.sortColumn = this.sortColumn.split('.')[1]; - } + this.sortField = json.filter.order.substring(1); + // if (this.sortField.indexOf('.') > 0) { + // this.sortTable = this.sortField.split('.')[0]; + // this.sortField = this.sortField.split('.')[1]; + // } let ch = json.filter.order.charAt(0); this.direction = (ch == '^') ? 'asc' : 'desc'; } @@ -109,11 +125,11 @@ export abstract class DataModelList { if (json && json.filter && json.filter.facets && json.filter.facets.length > 0) { for (let i = 0; i < json.filter.facets.length; i++) { - let newFacetInfo = this.facetFactory.GetFacet( + let fieldInfo = this.facetFactory.GetFacet( this, json.filter.facets[i].facet, json.filter.facets[i].values, extra); - if (newFacetInfo.dataTable != "" && newFacetInfo.allowedValues.length > 0) { - this.filteringFacets.push(newFacetInfo); - this.facetsToFetch.push(newFacetInfo); + if (fieldInfo.table != "" && fieldInfo.allowedValues.length > 0) { + this.filteringFacets.push(fieldInfo); + this.facetsToFetch.push(fieldInfo); } } } @@ -132,8 +148,9 @@ export abstract class DataModelList { } getCountQuery(): any { - let query = this.database(this.rootTable) - .select(this.database.raw('count(distinct ' + this.keyString() + ') as count')); + let queryDefinition = QueryDefinition.GenerateQueryDefinition(this, + [{table: this.rootTable, column: this.keyColumn, group_method: 'count', alias: 'count'} as FieldInfo]); + const query = queryDefinition.generateBaseQuery(false); this.addFacetConstraints(query, this.filteringFacets); this.addModelSpecificFiltering(query); this.captureQueryPerformance(query, "list count"); @@ -141,138 +158,109 @@ export abstract class DataModelList { }; getListQuery() { - const that = this; - let dataFields: RequestedData[]; + let dataFields: FieldInfo[]; if (this.fields && this.fields.length > 0) { - dataFields = Config.GetDataFields(this.rootTable, this.fields, this.databaseConfig); - } - else { - dataFields = Config.GetDataFieldsFromKey(this.listQueryKey(), this.sortTable, this.sortColumn); - } - const queryDefinition = QueryDefinition.GenerateQueryDefinition(this.rootTable, dataFields); - let rootTableObject = queryDefinition.getRootTable(); - if (rootTableObject == undefined) { - return; + dataFields = this.GetDataFields(); + } else { + dataFields = this.getDefaultFields(); } - let aggregateAll = (this.databaseConfig.getPrimaryKey(this.rootTable) != this.keyColumn); - - let leftJoins = queryDefinition.getLeftJoinTables(); - let innerJoins = queryDefinition.getInnerJoinTables(); - if(this.associatedDisease && this.rootTable != 'disease'){ - const hasDiseaseAlready = innerJoins.find(table => table.tableName === 'disease'); - if(!hasDiseaseAlready){ - innerJoins.push(new SqlTable('disease')); - } + if(!dataFields.map(f => f.name).includes(this.sortField)) { + this.pushOneDataField(this.sortField, dataFields); } + const sortField = dataFields.find(f => f.name === this.sortField); + + const queryDefinition = QueryDefinition.GenerateQueryDefinition(this, dataFields); + + const query = queryDefinition.generateBaseQuery(false); - let query = this.database(queryDefinition.getTablesAsObjectArray(innerJoins)) - .select(queryDefinition.getColumnList(this.database)); - if (aggregateAll) { - query.count({count: this.databaseConfig.getPrimaryKey(this.rootTable)}); - } this.addFacetConstraints(query, this.filteringFacets); - for (let i = 0; i < leftJoins.length; i++) { - let linkInfo = this.databaseConfig.getLinkInformation(rootTableObject.tableName, leftJoins[i].tableName); - if (!linkInfo) throw new Error("bad table configuration: " + rootTableObject?.tableName + " + " + leftJoins[i].tableName); - query.leftJoin(leftJoins[i].tableName + (leftJoins[i].alias ? ' as ' + leftJoins[i].alias : ''), function (this: any) { - // @ts-ignore - this.on(rootTableObject.tableName + "." + linkInfo.fromCol, '=', leftJoins[i].alias + "." + linkInfo.toCol); - if (leftJoins[i].joinConstraint) { - this.andOn(that.database.raw(leftJoins[i].joinConstraint)); - } - leftJoins[i].columns.forEach(col => { - if(col.where_clause){ - this.andOn(that.database.raw(col.where_clause)); - } - }); - }); - } - if (this.associatedDisease){ - query.where('disease.ncats_name', 'in', DiseaseList.getDescendentsQuery(this.database, this.associatedDisease)); - } - for (let i = 0; i < innerJoins.length; i++) { - if (rootTableObject !== innerJoins[i]) { - let leftTable = rootTableObject; - for (let j = 0; j < innerJoins[i].linkingTables.length; j++) { - let linkInfo = this.databaseConfig.getLinkInformation(leftTable.tableName, innerJoins[i].linkingTables[j]); - if (!linkInfo) throw new Error("bad table configuration: " + leftTable.tableName + " + " + innerJoins[i].linkingTables[j]); - query.whereRaw(leftTable.alias + "." + linkInfo.fromCol + "=" + innerJoins[i].linkingTables[j] + "." + linkInfo.toCol); - const additionalWhereClause = DatabaseTable.additionalWhereClause(innerJoins[i].tableName, innerJoins[i].alias, this); - if (additionalWhereClause) { - query.andWhere(this.database.raw(additionalWhereClause)); - } - leftTable = new SqlTable(innerJoins[i].linkingTables[j]); - } - let linkInfo = this.databaseConfig.getLinkInformation(leftTable.tableName, innerJoins[i].tableName); - if (!linkInfo) throw new Error("bad table configuration: " + leftTable.tableName + " + " + innerJoins[i].tableName); - query.whereRaw(leftTable.alias + "." + linkInfo.fromCol + "=" + innerJoins[i].alias + "." + linkInfo.toCol); - const additionalWhereClause = DatabaseTable.additionalWhereClause(innerJoins[i].tableName, innerJoins[i].alias, this); - if (additionalWhereClause) { - query.andWhere(this.database.raw(additionalWhereClause)); - } - innerJoins[i].columns.forEach(col => { - if(col.where_clause){ - query.andWhere(that.database.raw(col.where_clause)); - } - }); - } - } this.addModelSpecificFiltering(query, true); - if(this.fields.length === 0) { + if (queryDefinition.hasGroupedColumns()) { query.groupBy(this.keyString()); } - this.addSort(query, queryDefinition); + this.addSort(query, queryDefinition, sortField); if (this.skip) { query.offset(this.skip); } if (this.top) { query.limit(this.top); } - this.captureQueryPerformance(query, "list count"); return query; + } - addSort(query: any, queryDefinition: QueryDefinition) { - if (!this.sortColumn) { + addSort(query: any, queryDefinition: QueryDefinition, sortFieldInfo: FieldInfo | undefined) { + if (!this.sortField || !sortFieldInfo) { query.orderBy(this.defaultSortParameters()); return; } - const columnObj = queryDefinition.getColumnObj(this.sortTable, this.sortColumn); - + const sortTable = queryDefinition.getSqlTable(sortFieldInfo); let col = ""; - if(columnObj){ - if (columnObj.group_method) { - col = columnObj.group_method + "(" + this.sortTable + "." + columnObj.alias + ")"; - } else { - col = this.sortTable + "." + columnObj.alias; - } - } - else{ - col = this.sortColumn; + if (sortFieldInfo.group_method) { + col = sortFieldInfo.group_method + "(`" + sortFieldInfo.alias + "`)"; + } else { + col = "`" + sortFieldInfo.alias + "`"; } let dir = this.direction; - if(this.sortTable === "disease" && this.sortColumn === "pvalue"){ // workaround TCRD bug https://github.com/unmtransinfo/TCRD/issues/3 + if (sortTable.tableName === "disease" && sortFieldInfo.column === "pvalue") { // workaround TCRD bug https://github.com/unmtransinfo/TCRD/issues/3 query.orderByRaw((dir === "asc" ? "-" : "") + col + " + 0.0 desc") - } - else if( this.databaseConfig.tables.find(t => {return t.tableName === this.sortTable})?.columnIsNumeric(this.sortColumn)){ + } else if (this.databaseConfig.tables.find(t => t.tableName === sortTable.tableName)?.columnIsNumeric(sortFieldInfo.column)) { query.orderByRaw((dir === "asc" ? "-" : "") + col + " desc"); - } - else{ + } else { query.orderBy(col, dir); } } - addFacetConstraints(query: any, filteringFacets: FacetInfo[], facetToIgnore?: string) { + addFacetConstraints(query: any, filteringFacets: FieldInfo[], facetToIgnore?: string) { for (let i = 0; i < filteringFacets.length; i++) { - if (facetToIgnore == null || facetToIgnore != filteringFacets[i].type) { - const sqAlias = filteringFacets[i].type; + if (facetToIgnore == null || facetToIgnore != filteringFacets[i].name) { + const sqAlias = filteringFacets[i].name; let subQuery = filteringFacets[i].getFacetConstraintQuery().as(sqAlias); query.join(subQuery, sqAlias + '.' + this.keyColumn, this.keyString()); } } } + isNull() { + if (this.batch.length > 0) { + return false; + } + if (this.term.length > 0) { + return false; + } + if (this.associatedTarget.length > 0) { + return false; + } + if (this.associatedDisease.length > 0) { + return false; + } + if (this.filteringFacets.length > 0) { + return false; + } + return true; + } + + GetDataFields(): FieldInfo[] { + const dataFields: FieldInfo[] = []; + + dataFields.push(new FieldInfo({table: this.modelInfo.table, column: this.modelInfo.column, alias: 'id'} as FieldInfo)); + + this.fields.forEach(field => { + this.pushOneDataField(field, dataFields); + }); + return dataFields; + } + + + private pushOneDataField(field: string, dataFields: FieldInfo[]) { + const fieldInfo = this.databaseConfig.getOneField(this.modelInfo.name, '', this.getAssociatedModel(), '', field); + if (fieldInfo) { + fieldInfo.alias = field; + dataFields.push(fieldInfo); + } + } + perfData: QueryPerformanceData[] = []; captureQueryPerformance(query: any, description: string) { diff --git a/src/models/FacetFactory.ts b/src/models/FacetFactory.ts index 6971926..96cfe4b 100644 --- a/src/models/FacetFactory.ts +++ b/src/models/FacetFactory.ts @@ -1,15 +1,15 @@ import {DataModelList} from "./DataModelList"; -import {FacetInfo} from "./FacetInfo"; +import {FieldInfo} from "./FieldInfo"; export abstract class FacetFactory { - abstract GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FacetInfo; + abstract GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FieldInfo; - getFacetsFromList(parent: DataModelList, list: string[], extra?: any): FacetInfo[] { - let facetList: FacetInfo[] = []; + getFacetsFromList(parent: DataModelList, list: string[], extra?: any): FieldInfo[] { + let facetList: FieldInfo[] = []; if (list && list.length > 0) { for (let i = 0; i < list.length; i++) { let newFacet = this.GetFacet(parent, list[i], [], extra); - if (newFacet.dataTable != "") { + if (newFacet.table != "") { facetList.push(newFacet); } } @@ -17,34 +17,8 @@ export abstract class FacetFactory { return facetList; } - unknownFacet(): FacetInfo { - return new FacetInfo({} as FacetInfo); + unknownFacet(): FieldInfo { + return new FieldInfo({} as FieldInfo); } - parse_facet_config(parent: DataModelList, typeName: string, allowedValues: string[], nullQuery: boolean, facet_config: any) { - const hasNullQueryFields = () => { - return !!facet_config?.null_count_column; - }; - - const returnObj: FacetInfo = {} as FacetInfo; - returnObj.type = typeName; - returnObj.parent = parent; - returnObj.allowedValues = allowedValues; - returnObj.select = facet_config?.select; - returnObj.dataType = facet_config?.dataType || 'category'; - returnObj.binSize = facet_config?.binSize; - returnObj.log = facet_config?.log; - returnObj.sourceExplanation = facet_config?.sourceExplanation; - if (nullQuery && hasNullQueryFields()) { - returnObj.dataTable = facet_config?.null_table; - returnObj.dataColumn = facet_config?.null_column; - returnObj.whereClause = facet_config?.null_where_clause; - returnObj.countColumn = facet_config?.null_count_column; - } else { - returnObj.dataTable = facet_config?.dataTable; - returnObj.dataColumn = facet_config?.dataColumn; - returnObj.whereClause = facet_config?.whereClause; - } - return returnObj; - } } diff --git a/src/models/FacetInfo.ts b/src/models/FacetInfo.ts deleted file mode 100644 index bd10c78..0000000 --- a/src/models/FacetInfo.ts +++ /dev/null @@ -1,160 +0,0 @@ -import {DataModelList} from "./DataModelList"; - -export enum FacetDataType { - category="category", - numeric="numeric" -} - -export class FacetInfo { - type: string; - typeModifier: string; - dataTable: string; - dataColumn: string; - countColumn: string; // for precalculated tables - whereClause: string; - valuesDelimited: boolean; - select: string; - allowedValues: string[]; - parent: DataModelList; - tables: string[] = []; - dataType: FacetDataType; - binSize: number; - log: boolean; - sourceExplanation: string; - - constructor(obj: FacetInfo) { - this.dataType = obj?.dataType || FacetDataType.category; - this.binSize = obj?.binSize || 1; - this.log = obj?.log || false; - this.type = obj?.type || ""; - this.typeModifier = obj?.typeModifier || ""; - this.dataTable = obj?.dataTable || ""; - this.dataColumn = obj?.dataColumn || ""; - this.countColumn = obj?.countColumn || ""; - this.whereClause = obj?.whereClause || ""; - this.sourceExplanation = obj?.sourceExplanation || ""; - this.valuesDelimited = obj?.valuesDelimited || false; - this.select = obj?.log ? (`log(${this.dataString()})`) : obj?.select || this.dataString(); - this.allowedValues = obj?.allowedValues || []; - this.parent = obj?.parent || {}; - - if (obj && obj.parent) { - this.tables = this.parent.getRequiredTablesForFacet(this); - } - } - - dataString() { - return this.dataTable + "." + this.dataColumn; - } - - numericBounds() { - const scrubText = function (text: string): number | null { - if (!text) return null; - let retVal = text.replace(/[^0-9|\-|\.]/g, ''); - if (!retVal) return null; - return +retVal; - }; - - if (this.dataType !== FacetDataType.numeric || this.allowedValues.length < 1) { - return null; - } - let pieces = this.allowedValues[0].split(','); - return { - min: scrubText(pieces[0]), - max: scrubText(pieces[1]), - includeLower: pieces[0].includes('(') ? false : true, // default is to include the lower bound, unless overriden, because that's how a histogram would work - includeUpper: pieces[1].includes(']') ? true : false // default is to exclude the upper bound, unless overriden - }; - } - - getFacetConstraintQuery() { - let query = this.parent.database(DataModelList.listToObject(this.tables, this.parent.rootTable)) - .distinct(this.parent.keyString()); - if (this.dataType === FacetDataType.numeric) { - const bounds = this.numericBounds(); - if (bounds) { - if (bounds.min !== null) { - query.where(this.parent.database.raw(this.select), (bounds.includeLower ? ">=" : ">"), bounds.min); - } - if (bounds.max !== null) { - query.where(this.parent.database.raw(this.select), (bounds.includeUpper ? "<=" : "<"), bounds.max); - } - } - } else { - if (this.valuesDelimited) { - query.where(this.parent.database.raw(`${this.select} REGEXP '${this.allowedValues.join('|')}'`)); - } else { - query.whereIn(this.parent.database.raw(this.select), this.allowedValues); - } - } - if (this.whereClause.length > 0) { - query.whereRaw(this.whereClause); - } - if (this.dataTable != this.parent.rootTable) { - this.parent.addLinkToRootTable(query, this); - } - return query; - } - - getFacetQuery() { - if (this.dataTable == "") { - return null; - } - if (this.dataType === FacetDataType.numeric) { - let query = this.parent.database(DataModelList.listToObject(this.tables, this.parent.rootTable)) - .select(this.parent.database.raw(`floor(${this.select} / ${this.binSize}) * ${this.binSize} as bin, count(distinct ${this.parent.keyString()}) as value`)); - this.parent.addFacetConstraints(query, this.parent.filteringFacets, this.type); - this.parent.addModelSpecificFiltering(query); - if (this.dataTable != this.parent.rootTable) { - this.parent.addLinkToRootTable(query, this); - } - if (this.whereClause.length > 0) { - query.whereRaw(this.whereClause); - } - if (this.log) { - query.where(this.dataString(), ">", 0); - } else { - query.whereNotNull(this.dataString()); - } - query.groupBy('bin'); - this.parent.captureQueryPerformance(query, this.type); - return query; - } - if (this.countColumn != "") { - let query = this.parent.database(this.dataTable) - .select(this.parent.database.raw(`${this.dataColumn} as name, ${this.countColumn} as value`)); - if (this.whereClause.length > 0) { - query.whereRaw(this.whereClause); - } - query.orderBy('value', 'desc'); - - this.parent.captureQueryPerformance(query, this.type); - return query; - } - let query = this.parent.database(DataModelList.listToObject(this.tables, this.parent.rootTable)) - .select(this.parent.database.raw(`${this.select} as name, count(distinct ${this.parent.keyString()}) as value, ${this.select} as grouper_column`)); - this.parent.addFacetConstraints(query, this.parent.filteringFacets, this.type); - if (this.whereClause.length > 0) { - query.whereRaw(this.whereClause); - } - this.parent.addModelSpecificFiltering(query); - if (this.dataTable != this.parent.rootTable) { - this.parent.addLinkToRootTable(query, this); - } - query.groupBy('grouper_column').orderBy('value', 'desc'); - - this.parent.captureQueryPerformance(query, this.type); - return query; - } - - static deduplicate(array: FacetInfo[]) { - var newArray = array.concat(); - for (var i = 0; i < newArray.length; ++i) { - for (var j = i + 1; j < newArray.length; ++j) { - if (newArray[i].type === newArray[j].type) - newArray.splice(j--, 1); - } - } - return newArray; - } -} diff --git a/src/models/FieldInfo.ts b/src/models/FieldInfo.ts new file mode 100644 index 0000000..9b995ad --- /dev/null +++ b/src/models/FieldInfo.ts @@ -0,0 +1,256 @@ +import {DataModelList} from "./DataModelList"; +import {QueryDefinition} from "./queryDefinition"; + +export enum FacetDataType { + category = 'category', + numeric = 'numeric' +} + +export class FieldInfo { + name: string; + description: string; + + table: string; + column: string; + alias: string; + select: string; + where_clause: string; + group_method: string; + + null_table: string; + null_column: string; + null_count_column: string; + null_where_clause: string; + + dataType: FacetDataType; + binSize: number; + log: boolean; + + order: number; + default: boolean; + + needsDistinct: boolean; + typeModifier: string; + valuesDelimited: boolean; + allowedValues: string[]; + parent: DataModelList; + + isFromListQuery: boolean; + + constructor(obj: any) { + this.name = obj?.name || ''; + this.description = obj?.description || ''; + + this.table = obj?.table || ''; + this.column = obj?.column || ''; + this.alias = obj?.alias || this.column; + this.where_clause = obj?.where_clause || ''; + this.group_method = obj?.group_method || ''; + + this.null_table = obj?.null_table || ''; + this.null_column = obj?.null_column || ''; + this.null_count_column = obj?.null_count_column || ''; + this.null_where_clause = obj?.null_where_clause || ''; + + this.dataType = obj?.dataType || FacetDataType.category; + this.binSize = obj?.binSize || 1; + this.log = obj?.log || false; + + this.order = obj?.order; + this.default = obj?.default || false; + + this.needsDistinct = obj?.needsDistinct || false; + this.typeModifier = obj?.typeModifier || ""; + this.valuesDelimited = obj?.valuesDelimited || false; + this.allowedValues = obj?.allowedValues || []; + this.parent = obj?.parent || {}; + + this.isFromListQuery = obj?.isFromListQuery || false; + + this.select = obj?.log ? (`log(${this.dataString()})`) : obj?.select || this.dataString(); + } + + dataString() { + return this.table + '.' + this.column; + } + + copy() { + return new FieldInfo(this); + } + + // static parseFromConfigTable(parent: DataModelList, typeName: string, allowedValues: string[], nullQuery: boolean, facet_config: any): FieldInfo { + // const hasNullQueryFields = () => { + // return !!facet_config?.null_count_column; + // }; + // + // const returnObj: FieldInfo = {} as FieldInfo; + // returnObj.name = typeName; + // returnObj.parent = parent; + // returnObj.allowedValues = allowedValues; + // returnObj.select = facet_config?.select; + // returnObj.dataType = facet_config?.dataType || 'category'; + // returnObj.binSize = facet_config?.binSize; + // returnObj.log = facet_config?.log; + // returnObj.sourceExplanation = facet_config?.sourceExplanation; + // returnObj.groupMethod = facet_config?.group_method; + // if (nullQuery && hasNullQueryFields()) { + // returnObj.dataTable = facet_config?.null_table; + // returnObj.dataColumn = facet_config?.null_column; + // returnObj.whereClause = facet_config?.null_where_clause; + // returnObj.countColumn = facet_config?.null_count_column; + // } else { + // returnObj.dataTable = facet_config?.dataTable; + // returnObj.dataColumn = facet_config?.dataColumn; + // returnObj.whereClause = facet_config?.whereClause; + // } + // return new FieldInfo(returnObj); + // } + + numericBounds() { + const scrubText = function (text: string): number | null { + if (!text) return null; + let retVal = text.replace(/[^0-9|\-|\.]/g, ''); + if (!retVal) return null; + return +retVal; + }; + + if (this.dataType !== FacetDataType.numeric || this.allowedValues.length < 1) { + return null; + } + let pieces = this.allowedValues[0].split(','); + return { + min: scrubText(pieces[0]), + max: scrubText(pieces[1]), + includeLower: pieces[0].includes('(') ? false : true, // default is to include the lower bound, unless overriden, because that's how a histogram would work + includeUpper: pieces[1].includes(']') ? true : false // default is to exclude the upper bound, unless overriden + }; + } + + getFacetConstraintQuery() { + const queryDefinition = QueryDefinition.GenerateQueryDefinition( + { + database: this.parent.database, + databaseConfig: this.parent.databaseConfig, + associatedTarget: this.parent.associatedTarget, + associatedDisease: this.parent.associatedDisease, + rootTable: this.table, + ppiConfidence: this.parent.ppiConfidence, + getSpecialModelWhereClause: this.parent.getSpecialModelWhereClause + }, [ + new FieldInfo({ + table: this.parent.rootTable, + column: this.parent.keyColumn, + needsDistinct: true + } as FieldInfo) + ]); + const query = queryDefinition.generateBaseQuery(true); + + if (this.dataType === FacetDataType.numeric) { + const bounds = this.numericBounds(); + if (bounds) { + if (bounds.min !== null) { + query.where(this.parent.database.raw(this.select), (bounds.includeLower ? ">=" : ">"), bounds.min); + } + if (bounds.max !== null) { + query.where(this.parent.database.raw(this.select), (bounds.includeUpper ? "<=" : "<"), bounds.max); + } + } + } else { + if (this.valuesDelimited) { + query.where(this.parent.database.raw(`${this.select} REGEXP '${this.allowedValues.join('|')}'`)); + } else { + query.whereIn(this.parent.database.raw(this.select), this.allowedValues); + } + } + if (this.where_clause.length > 0) { + query.whereRaw(this.where_clause); + } + query.whereNotNull(`${this.parent.rootTable}.${this.parent.keyColumn}`); + return query; + } + + getFacetQuery() { + if (this.table == "") { + return null; + } + let query; + if (this.parent.isNull() && this.null_count_column) { + query = this.getPrecalculatedFacetQuery(); + } else if (this.dataType === FacetDataType.numeric) { + query = this.getNumericFacetQuery(); + } else { + query = this.getStandardFacetQuery(); + } + this.parent.captureQueryPerformance(query, this.name); + return query; + } + + private getPrecalculatedFacetQuery() { + let query = this.parent.database(this.null_table).select({ + name: this.null_column, + value: this.null_count_column + }); + if (this.null_where_clause.length > 0) { + query.whereRaw(this.null_where_clause); + } + query.orderBy('value', 'desc'); + return query; + } + + private getStandardFacetQuery() { + let queryDefinition = QueryDefinition.GenerateQueryDefinition(this.parent, + [ + {...this, alias: 'name'}, + (this.getCountColumnInfo()) + ]); + + let query = queryDefinition.generateBaseQuery(true); + + this.parent.addFacetConstraints(query, this.parent.filteringFacets, this.name); + this.parent.addModelSpecificFiltering(query); + query.groupBy(1).orderBy('value', 'desc'); + return query; + } + + private getNumericFacetQuery() { + let queryDefinition = QueryDefinition.GenerateQueryDefinition(this.parent, + [ + { + ...this, + select: `floor(${this.select} / ${this.binSize}) * ${this.binSize}`, + alias: 'bin' + }, + (this.getCountColumnInfo()) + ]); + let query = queryDefinition.generateBaseQuery(true); + this.parent.addFacetConstraints(query, this.parent.filteringFacets, this.name); + this.parent.addModelSpecificFiltering(query); + if (this.log) { + query.where(this.dataString(), ">", 0); + } else { + query.whereNotNull(this.dataString()); + } + query.groupBy('bin'); + return query; + } + + private getCountColumnInfo() { + return new FieldInfo({ + table: this.parent.rootTable, + column: this.parent.keyColumn, + group_method: 'count', + alias: 'value' + } as FieldInfo); + } + + static deduplicate(array: FieldInfo[]) { + var newArray = array.concat(); + for (var i = 0; i < newArray.length; ++i) { + for (var j = i + 1; j < newArray.length; ++j) { + if (newArray[i].name === newArray[j].name) + newArray.splice(j--, 1); + } + } + return newArray; + } +} diff --git a/src/models/IBuildable.ts b/src/models/IBuildable.ts new file mode 100644 index 0000000..f6ce1d7 --- /dev/null +++ b/src/models/IBuildable.ts @@ -0,0 +1,15 @@ +import {DatabaseConfig} from "./databaseConfig"; +import * as Knex from "knex"; +import {FieldInfo} from "./FieldInfo"; + +export interface IBuildable { + database: Knex; + databaseConfig: DatabaseConfig; + + associatedTarget: string; + associatedDisease: string; + rootTable: string; + ppiConfidence: number; + + getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string; +} diff --git a/src/models/config.ts b/src/models/config.ts index 31fa66a..084afa4 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -1,23 +1,10 @@ -import {DatabaseTable} from "./databaseTable"; -import {DatabaseConfig} from "./databaseConfig"; +import {FieldInfo} from "./FieldInfo"; +import {QueryDefinition} from "./queryDefinition"; /** * Class for gathering tables and columns to use for standard queries */ export class Config { - static GetDataFields(rootTable: string, fields: string[], dbConfig: DatabaseConfig): RequestedData[] { - const dataFields: RequestedData[] = []; - - // TODO : get primary key better - dataFields.push({table: "protein", data: "id", alias: 'id'}); - - fields.forEach(field => { - const facetInfo = dbConfig.getFacetConfig(rootTable, field); - dataFields.push({table: facetInfo.dataTable, data: facetInfo.dataColumn, alias: field, where_clause: facetInfo.whereClause, group_method: facetInfo.group_method}); - }); - return dataFields; - } - /** * TODO : get rid of this when the normal list pages are using the pharos_config.fieldList * @param fieldKey @@ -25,321 +12,44 @@ export class Config { * @param sortColumn * @constructor */ - static GetDataFieldsFromKey(fieldKey: ConfigKeys, sortTable: string = "", sortColumn: string = ""): RequestedData[] { - const dataFields: RequestedData[] = []; + static GetDataFieldsFromKey(fieldKey: ConfigKeys, sortTable: string = "", sortColumn: string = ""): FieldInfo[] { + const dataFields: FieldInfo[] = []; switch (fieldKey) { - case ConfigKeys.Target_List_Default: - dataFields.push({table: "target", data: "id", alias: "tcrdid"}); - dataFields.push({table: "target", data: "tdl"}); - dataFields.push({table: "target", data: "fam"}); - - dataFields.push({table: "protein", data: "description", alias: "name"}); - dataFields.push({table: "protein", data: "uniprot"}); - dataFields.push({table: "protein", data: "sym"}); - dataFields.push({table: "protein", data: "seq"}); - - dataFields.push({table: "tinx_novelty", data: "score", alias: "novelty"}); - dataFields.push({table: "tdl_info", data: "NCBI Gene Summary", alias: "description"}); - break; - case ConfigKeys.Target_List_PPI: - dataFields.push({table: "target", data: "id", alias: "tcrdid"}); - dataFields.push({table: "target", data: "tdl"}); - dataFields.push({table: "target", data: "fam"}); - - dataFields.push({table: "protein", data: "description", alias: "name"}); - dataFields.push({table: "protein", data: "sym"}); - dataFields.push({table: "protein", data: "uniprot"}); - dataFields.push({table: "protein", data: "seq"}); - - dataFields.push({table: "tinx_novelty", data: "score", alias: "novelty"}); - - dataFields.push({table: "ncats_ppi", data: "ppitypes"}); - dataFields.push({table: "ncats_ppi", data: "interaction_type"}); - dataFields.push({table: "ncats_ppi", data: "evidence"}); - dataFields.push({table: "ncats_ppi", data: "score"}); - dataFields.push({table: "ncats_ppi", data: "p_ni"}); - dataFields.push({table: "ncats_ppi", data: "p_int"}); - dataFields.push({table: "ncats_ppi", data: "p_wrong"}); - break; - case ConfigKeys.Target_List_Disease: - dataFields.push({table: "target", data: "id", alias: "tcrdid"}); - dataFields.push({table: "target", data: "tdl"}); - dataFields.push({table: "target", data: "fam"}); - - dataFields.push({table: "protein", data: "description", alias: "name"}); - dataFields.push({table: "protein", data: "sym"}); - dataFields.push({table: "protein", data: "uniprot"}); - dataFields.push({table: "protein", data: "seq"}); - - dataFields.push({table: "tinx_novelty", data: "score", alias: "novelty"}); - - dataFields.push({table: "disease", data: `dtype`, group_method: `count`}); - break; case ConfigKeys.Target_List_Similarity: - dataFields.push({table: "target", data: "id", alias: "tcrdid"}); - dataFields.push({table: "target", data: "tdl"}); - dataFields.push({table: "target", data: "fam"}); - - dataFields.push({table: "protein", data: "description", alias: "name"}); - dataFields.push({table: "protein", data: "sym"}); - dataFields.push({table: "protein", data: "uniprot"}); - dataFields.push({table: "protein", data: "seq"}); - - dataFields.push({table: "tinx_novelty", data: "score", alias: "novelty"}); - - dataFields.push({subQuery: true, table: "similarityQuery", data:"jaccard"}); - dataFields.push({subQuery: true, table: "similarityQuery", data:"overlap"}); - dataFields.push({subQuery: true, table: "similarityQuery", data:"baseSize"}); - dataFields.push({subQuery: true, table: "similarityQuery", data:"testSize"}); - dataFields.push({subQuery: true, table: "similarityQuery", data:"commonOptions"}); break; case ConfigKeys.Disease_List_Default: - dataFields.push({table: "disease", data: "ncats_name", alias: "name"}); + dataFields.push({table: "disease", column: "ncats_name", alias: "name"} as FieldInfo); break; case ConfigKeys.Ligand_List_Default: - dataFields.push({table: "ncats_ligands", data: "identifier", alias: "ligid"}); - dataFields.push({table: "ncats_ligands", data: "isDrug", alias: "isdrug"}); - dataFields.push({table: "ncats_ligands", data: "name"}); - dataFields.push({table: "ncats_ligands", data: "smiles"}); - dataFields.push({table: "ncats_ligands", data: "actCnt", alias: "actcnt"}); - dataFields.push({table: "ncats_ligands", data: "PubChem"}); - dataFields.push({table: "ncats_ligands", data: "ChEMBL"}); - dataFields.push({table: "ncats_ligands", data: "Guide to Pharmacology"}); - dataFields.push({table: "ncats_ligands", data: "DrugCentral"}); - dataFields.push({table: "ncats_ligands", data: "description"}); + dataFields.push({table: "ncats_ligands", column: "identifier", alias: "ligid"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "isDrug", alias: "isdrug"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "name"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "smiles"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "actCnt", alias: "actcnt"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "PubChem"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "ChEMBL"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "Guide to Pharmacology"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "DrugCentral"} as FieldInfo); + dataFields.push({table: "ncats_ligands", column: "description"} as FieldInfo); break; } if (sortTable && sortColumn && !dataFields.find(field => { - return field.data === sortColumn && field.table === sortTable + return field.column === sortColumn && field.table === sortTable }) && !dataFields.find(field => { - return field.alias === sortColumn && field.table === sortTable + return field.alias === sortColumn && field.table === sortTable })) { - dataFields.push({table: sortTable, data: sortColumn, alias: sortColumn, group_method:"max"}); + dataFields.push({table: sortTable, column: sortColumn, alias: sortColumn, group_method:"max"} as FieldInfo); } else if (sortColumn && !dataFields.find(field => { - return field.data === sortColumn + return field.column === sortColumn }) && !dataFields.find(field => { return field.alias === sortColumn })) { - dataFields.push({table: sortTable, data: sortColumn, alias: sortColumn, group_method:"max"}); + dataFields.push({table: sortTable, column: sortColumn, alias: sortColumn, group_method:"max"} as FieldInfo); } return dataFields; } } -/** - * Class to hold details for what columns the user wants to retrieve - */ -export class RequestedData { - table: string = ""; - data: string = ""; - alias?: string = ""; - group_method?: string = ""; - where_clause?: string = ""; - subQuery?: boolean = false; -} - -/** - * Class to hold all the details required to generate a query given a required set of data - */ -export class QueryDefinition { - tables: SqlTable[] = []; - rootTable: string; - - constructor(rootTable: string) { - this.rootTable = rootTable; - } - - static GenerateQueryDefinition(rootTable: string, dataList: RequestedData[]): QueryDefinition { - let qd = new QueryDefinition(rootTable); - for (let i = 0; i < dataList.length; i++) { - qd.addRequestedData(dataList[i]); - } - return qd; - } - - addRequestedData(reqData: RequestedData) { - if (DatabaseTable.leftJoinTables.includes(reqData.table)) { - this.addRequestedDataToNewTable(reqData); - return; - } - const existingTable = this.tables.find(table => { - return table.tableName == reqData.table; - }); - if (existingTable) { - existingTable.columns.push(new SqlColumns(reqData.data, reqData.alias, reqData.group_method)); - return; - } - this.addRequestedDataToNewTable(reqData); - } - - addRequestedDataToNewTable(reqData: RequestedData) { - let links: string[] = []; - const tableCount = this.tables.filter(t => { - return t.tableName == reqData.table; - }).length; - - if (DatabaseTable.sparseTables.includes(reqData.table)) { - this.tables.push(SqlTable.getSparseTableData(reqData.table, reqData.data, reqData.alias, reqData.table + tableCount, reqData.group_method, reqData.where_clause)); - return; - } - - if (DatabaseTable.typeTables.includes(reqData.table)) { - const typeColumn = DatabaseTable.typeTableColumns.get(reqData.table); - if (!typeColumn) throw new Error(`bad table configuration - ${reqData.table} has no type column configuration`); - const dataColumn = DatabaseTable.typeTableColumnMapping.get(reqData.table + "-" + reqData.data); - this.tables.push( - SqlTable.getTypeTableData( - reqData.table, - reqData.data, - typeColumn, - (dataColumn || reqData.data), - reqData.alias, - reqData.table + tableCount) - ); - return; - } - - if (reqData.table != this.rootTable) { - links = DatabaseTable.getRequiredLinks(reqData.table, this.rootTable) || []; - } - - const newTable = new SqlTable(reqData.table, {}, links, reqData.subQuery); - newTable.columns.push(new SqlColumns(reqData.data, reqData.alias, reqData.group_method, reqData.where_clause)); - this.tables.push(newTable); - } - - getColumnObj(table: string, column: string) { - let matchingTables = this.tables.filter(t => {return t.tableName == table;}); - for(let i = 0 ; i < matchingTables.length ; i++){ - let matchingColumns = matchingTables[i].columns.filter(c => {return c.column === column}); - if(matchingColumns.length > 0){ - return matchingColumns[0]; - } - } - return null; - } - - getColumnList(db: any) { - const columnList: any = {}; - for (let tableIndex = 0; tableIndex < this.tables.length; tableIndex++) { - for (let columnIndex = 0; columnIndex < this.tables[tableIndex].columns.length; columnIndex++) { - if (this.tables[tableIndex].columns[columnIndex].group_method) { - columnList[this.tables[tableIndex].columns[columnIndex].alias] = - db.raw(this.tables[tableIndex].columns[columnIndex].group_method - + '(distinct `' + this.tables[tableIndex].alias + "`.`" + this.tables[tableIndex].columns[columnIndex].column + '`)'); - } else { - columnList[this.tables[tableIndex].columns[columnIndex].alias] = - db.raw("`" + this.tables[tableIndex].alias + "`.`" + this.tables[tableIndex].columns[columnIndex].column + "`"); - } - } - } - return columnList; - } - - getInnerJoinTables(): SqlTable[] { - return this.tables.filter(table => { - if (table.subQuery) return false; - return table.allowUnmatchedRows == false; - }); - } - - getLeftJoinTables(): SqlTable[] { - return this.tables.filter(table => { - if (this.rootTable === table.tableName) return false; - if (table.subQuery) return false; - return table.allowUnmatchedRows == true; - }); - } - - getRootTable(): SqlTable | undefined { - return this.tables.find(table => { - return table.tableName == this.rootTable - }); - } - - getTablesAsObjectArray(tableList: SqlTable[]): any { - let obj: any = {}; - for (let i = 0; i < tableList.length; i++) { - if (tableList[i].tableName != this.rootTable) { - obj[tableList[i].alias] = tableList[i].tableName; - for (let j = 0; j < tableList[i].linkingTables.length; j++) { - obj[tableList[i].linkingTables[j]] = tableList[i].linkingTables[j]; - } - } - } - obj[this.rootTable] = this.rootTable; - return obj; - } -} - -export class SqlTable { - tableName: string; - private _alias?: string; - get alias(): string { - if (this._alias) return this._alias; - return this.tableName; - } - - allowUnmatchedRows?: boolean; - joinConstraint: string; - columns: SqlColumns[] = []; - linkingTables: string[] = []; - subQuery: boolean = false; - - constructor(tableName: string, - {alias = "", allowUnmatchedRows = false, joinConstraint = ""} = {}, - linkingTables: string[] = [], subQuery: boolean = false) { - this.tableName = tableName; - this.allowUnmatchedRows = allowUnmatchedRows; - this.joinConstraint = joinConstraint; - this.linkingTables = linkingTables; - this.subQuery = subQuery; - if (alias) { - this._alias = alias; - } - } - - static getTypeTableData(tableName: string, typeName: string, typeColumn: string, dataColumn: string, columnAlias?: string, tableAlias?: string, group_method?: string, where_clause?: string) { - const typeTable = new SqlTable(tableName, { - allowUnmatchedRows: true, - joinConstraint: `${tableAlias || tableName}.${typeColumn} = '${typeName}'`, - alias: tableAlias - }); - typeTable.columns.push(new SqlColumns(dataColumn, columnAlias || dataColumn, group_method, where_clause)); - return typeTable; - } - - static getSparseTableData(tableName: string, dataColumn: string, columnAlias?: string, tableAlias?: string, group_method?: string, where_clause?: string) { - const sparseTable = new SqlTable(tableName, {allowUnmatchedRows: true, alias: tableAlias}); - if(where_clause && tableAlias) { - where_clause = where_clause.replace(tableName, tableAlias); - } - sparseTable.columns.push(new SqlColumns(dataColumn, columnAlias || dataColumn, group_method, where_clause)); - return sparseTable; - } -} - -export class SqlColumns { - column: string; - private _alias?: string = ""; - group_method?: string = ""; - where_clause?: string = ""; - - get alias(): string { - if (this._alias) return this._alias; - return this.column; - } - - constructor(column: string, alias: string = "", group_method: string = "", where_clause: string = "") { - this.column = column; - this.group_method = group_method; - this.where_clause = where_clause; - if (alias) { - this._alias = alias; - } - } -} - export enum ConfigKeys { Target_List_Default, Target_List_PPI, diff --git a/src/models/databaseConfig.ts b/src/models/databaseConfig.ts index ee36c53..171a8a1 100644 --- a/src/models/databaseConfig.ts +++ b/src/models/databaseConfig.ts @@ -1,144 +1,176 @@ import {DatabaseTable, TableLink, TableLinkInfo} from "./databaseTable"; -import {FacetInfo} from "./FacetInfo"; +import {FieldInfo} from "./FieldInfo"; + + +export class FieldList { + model: string; + context: string; + associatedModel: string = ''; + listName: string = ''; + + constructor(model: string, context: string, associatedModel: string, listName: string) { + this.model = model; + this.context = context || ''; + this.associatedModel = associatedModel || ''; + this.listName = listName || ''; + } + + static parseJson(obj: any): FieldList { + return new FieldList(obj.model, obj.context, obj.associatedModel, obj.listName); + } + + static parseJsonContextFree(obj: any): FieldList { + return new FieldList(obj.model, '', obj.associatedModel, ''); + } + + get listKey(): string { + return `${this.model}-${this.context}-${this.associatedModel}-${this.listName}`.toLowerCase(); + } +} /** * Class to query the database to see what tables and foreign keys are there, to automatically generate the query for the requested data */ export class DatabaseConfig { tables: DatabaseTable[] = []; - modelList: Map = new Map(); - facetMap: Map = new Map(); - fieldLists: Map = new Map(); + availableFieldMap: Map = new Map(); + + getAvailableFields(model: string, context: string = '', associatedModel: string = '', listName: string = ''): FieldInfo[] { + const key = new FieldList(model, context, associatedModel, listName).listKey; + return this.availableFieldMap.get(key)?.map(each => each.copy()) || []; + } + + getDefaultFields(model: string, context: string = '', associatedModel: string = '', listName: string = ''): FieldInfo[] { + return this.getAvailableFields(model, context, associatedModel, listName) + .filter(fInfo => fInfo.default).sort(((a, b) => a.order - b.order)); + } + + getOneField(model: string, context: string = '', associatedModel: string, listName: string, name: string): FieldInfo | undefined { + const list = this.getAvailableFields(model, context, associatedModel, listName); + if (list.length > 0) { + return list.find((fieldInfo: FieldInfo) => fieldInfo.name === name)?.copy(); + } + return undefined; + } + + populateFieldLists() { + const query = this.database({...this.modelTable, ...this.fieldListTable, ...this.fieldTable, ...this.contextTable} + ).leftJoin({...this.assocModelTable}, 'associated_model.id', 'field_context.associated_model_id') + .select( + { + // list defining fields + model: 'model.name', + context: 'field_context.context', + associatedModel: 'associated_model.name', + listName: 'field_context.name', + + // field defining fields + name: 'field.name', + alias: 'field_list.alias', + order: 'field_list.order', + default: 'field_list.default', + description: this.database.raw('COALESCE(field_list.description, field.description)'), + + // where is the data + table: 'field.table', + column: 'field.column', + select: 'field.select', + where_clause: 'field.where_clause', + group_method: 'field.group_method', + + // when facets are precalculated + null_table: 'field.null_table', + null_column: 'field.null_column', + null_count_column: 'field.null_count_column', + null_where_clause: 'field.null_where_clause', + + // numeric facets + dataType: 'field.dataType', + binSize: 'field.binSize', + log: 'field.log' + }) + .where('field_context.model_id', this.database.raw('model.id')) + .andWhere('field_context.id', this.database.raw('field_list.context_id')) + .andWhere('field_list.field_id', this.database.raw('field.id')) + .orderBy( + [ + 'model.id', 'field_context.context', 'associated_model.id', 'field_context.name', + { + column: this.database.raw('-field_list.order'), + order: 'desc' + }, + 'field.table' + ]); + return query.then((rows: any[]) => { + rows.forEach(row => { + ['parseJson', 'parseJsonContextFree'].forEach((method: string) => { + // @ts-ignore + const key = FieldList[method](row).listKey; + const fieldInfo = new FieldInfo(row); + if (this.availableFieldMap.has(key)) { + const existingList = this.availableFieldMap.get(key) || []; + if(!existingList.map(f => f.name).includes(row.name)) { + existingList.push(fieldInfo); + } + } else { + this.availableFieldMap.set(key, [fieldInfo]); + } + }); + }); + }); + + } + + modelList: Map = new Map(); database: any; dbName: string; configDB: string; - private _fieldTable: { field: string } = {field: ''}; get fieldTable(): { field: string } { return {field: this.configDB + '.field'}; } - private _fieldListTable: { fieldList: string } = {fieldList: ''}; - get fieldListTable(): { fieldList: string } { - return {fieldList: this.configDB + '.fieldList'}; + get fieldListTable(): { field_list: string } { + return {field_list: this.configDB + '.field_list'}; } - private _modelTable: { model: string } = {model: ''}; get modelTable(): { model: string } { return {model: this.configDB + '.model'}; }; + get assocModelTable(): { associated_model: string } { + return {associated_model: this.configDB + '.model'}; + }; + + get contextTable(): { field_context: string } { + return {field_context: this.configDB + '.field_context'}; + } + + loadPromise: Promise; + constructor(database: any, dbName: string, configDB: string) { this.database = database; this.dbName = dbName; this.configDB = configDB; - - this.parseTables(); - this.parseFacets(); - this.parseLists(); + this.loadPromise = Promise.all([this.parseTables(), this.populateFieldLists(), this.loadModelMap()]); } parseTables() { let query = this.database.raw('show tables from ' + this.dbName); - query.then((rows: any) => { + return query.then((rows: any) => { for (let tableKey in rows[0]) { this.tables.push(new DatabaseTable(this.database, this.dbName, rows[0][tableKey]["Tables_in_" + this.dbName])); } + return Promise.all(this.tables.map(t => t.loadPromise)); }); } - parseFacets() { - let facetQuery = this.database({...this.fieldTable, ...this.modelTable}) - .select( - { - ...this.fieldColumns, - ...this.modelColumns - }).whereRaw(this.linkFieldToModel); - facetQuery.then((rows: any[]) => { - for (let row of rows) { - this.facetMap.set(`${row.rootTable}-${row.type}`, row); - } - }); - } - - fieldListColumns = { - listType: `fieldList.type`, - listName: `fieldList.listName`, - field_id: `fieldList.field_id`, - order: 'fieldList.order' - }; - fieldColumns = { - model_id: `field.model_id`, - type: `field.type`, - dataTable: `field.table`, - dataColumn: `field.column`, - select: `field.select`, - whereClause: `field.where_clause`, - null_table: `field.null_table`, - null_column: `field.null_column`, - null_count_column: `field.null_count_column`, - null_where_clause: `field.null_where_clause`, - dataType: `field.dataType`, - binSize: `field.binSize`, - log: `field.log`, - sourceExplanation: `field.description`, - isGoodForFacet: `field.isGoodForFacet` - }; - modelColumns = { - modelName: 'model.name', - rootTable: 'model.table', - rootColumn: 'model.column' - }; - linkFieldToModel = 'field.model_id = model.id'; - linkFieldListToField = 'fieldList.field_id = field.id'; - - parseLists() { - const listQuery = this.database({ - ...this.fieldTable, - ...this.fieldListTable, - ...this.modelTable - }) - .select({ - ...this.fieldListColumns, - ...this.fieldColumns, - ...this.modelColumns - }) - .whereRaw(this.linkFieldListToField) - .whereRaw(this.linkFieldToModel); - listQuery.then((rows: any[]) => { - rows.forEach(row => { - const listNameString = row.modelName + ' ' + row.listType + ' - ' + row.listName; - if (this.fieldLists.has(listNameString)) { - const list = this.fieldLists.get(listNameString); - list?.push(row); - } else { - this.fieldLists.set(listNameString, [row]); - } - }); - }); - const allFieldsQuery = this.database({...this.fieldTable, ...this.modelTable}) - .select({...this.fieldColumns, ...this.modelColumns}) - .whereRaw(this.linkFieldToModel); - allFieldsQuery.then((rows: any[]) => { + loadModelMap() { + let query = this.database({...this.modelTable}).select('*').then((rows: any[]) => { rows.forEach(row => { - const keyName = `${row.modelName} Facet - All`; - if(row.isGoodForFacet) { - if (this.fieldLists.has(keyName)) { - const list = this.fieldLists.get(keyName); - list?.push(row); - } else { - this.fieldLists.set(keyName, [row]); - } - if (!this.modelList.has(row.rootTable)) { - this.modelList.set(row.rootTable, {rootTable: row.rootTable, name: row.modelName}); - } - } + this.modelList.set(row.table, row); }); - }); - } - - getFacetConfig(rootTable: string, facetType: string) { - return this.facetMap.get(`${rootTable}-${facetType}`); + }) } getPrimaryKey(table: string): string { @@ -148,11 +180,14 @@ export class DatabaseConfig { return t?.primaryKey || 'id'; } - getLinkInformation(fromTable: string, toTable: string): TableLinkInfo | null { + getLinkInformation(fromTable: string, toTable: string): TableLinkInfo { let linkInfo = this._getLinkInformation(fromTable, toTable); if (!linkInfo) { linkInfo = this._getLinkInformation(toTable, fromTable, true); } + if (!linkInfo) { + throw new Error(`Error building query: Could not find link between ${fromTable} and ${toTable}`); + } return linkInfo; } @@ -181,7 +216,7 @@ export class DatabaseConfig { return new TableLinkInfo(toLink.column, toLink.otherColumn); } - getJoinTables(rootTable: string, dataTable: string): string[]{ + getJoinTables(rootTable: string, dataTable: string): string[] { let joinTables: string[] = []; if (dataTable != rootTable) { @@ -189,29 +224,28 @@ export class DatabaseConfig { joinTables.push(...links); joinTables.push(rootTable); } - return joinTables; + return joinTables; } - getBaseSetQuery(rootTable: string, facet: FacetInfo, columns?: any){ - const joinTables = this.getJoinTables(rootTable, facet.dataTable); + getBaseSetQuery(rootTable: string, facet: FieldInfo, columns?: any) { + const joinTables = this.getJoinTables(rootTable, facet.table); - const query = this.database(facet.dataTable); - if(!columns) { + const query = this.database(facet.table); + if (!columns) { query.distinct({value: this.database.raw(facet.select)}); - } - else{ + } else { query.select(columns); } - let leftTable = facet.dataTable; + let leftTable = facet.table; joinTables.forEach(rightTable => { const linkInfo = this.getLinkInformation(leftTable, rightTable); query.join(rightTable, `${leftTable}.${linkInfo?.fromCol}`, '=', `${rightTable}.${linkInfo?.toCol}`); leftTable = rightTable; }); - if (facet.whereClause.length > 0) { - query.whereRaw(facet.whereClause); + if (facet.where_clause.length > 0) { + query.whereRaw(facet.where_clause); } return query; } diff --git a/src/models/databaseTable.ts b/src/models/databaseTable.ts index d526690..841c75d 100644 --- a/src/models/databaseTable.ts +++ b/src/models/databaseTable.ts @@ -5,15 +5,14 @@ export class DatabaseTable { primaryKey?: string; links: TableLink[] = []; dataTypes: Map = new Map(); - isSparse: boolean; - isTypeTable: boolean; + loadPromise: Promise; constructor(database: any, dbname: string, name: string) { this.tableName = name; - this.isSparse = DatabaseTable.sparseTables.includes(name); - this.isTypeTable = DatabaseTable.typeTables.includes(name); - this.getKeys(database, dbname); - this.getColumnInfo(database, dbname); + this.loadPromise = Promise.all([ + this.getKeys(database, dbname), + this.getColumnInfo(database, dbname) + ]); } getColumnInfo(database: any, dbname: string) { @@ -21,7 +20,7 @@ export class DatabaseTable { .select(['column_name', 'data_type']) .where('table_schema', dbname) .andWhere('table_name', this.tableName); - query.then((rows: any) => { + return query.then((rows: any) => { for (let rowKey in rows) { this.dataTypes.set(rows[rowKey]['column_name'], rows[rowKey]['data_type']); } @@ -41,7 +40,7 @@ export class DatabaseTable { .select(['constraint_name', 'column_name', 'referenced_table_name', 'referenced_column_name']) .where('table_schema', dbname) .andWhere('table_name', this.tableName); - query.then((rows: any) => { + return query.then((rows: any) => { for (let rowKey in rows) { if (rows[rowKey]['constraint_name'] == 'PRIMARY') { this.primaryKey = rows[rowKey]['column_name'] @@ -55,85 +54,29 @@ export class DatabaseTable { }); } - // Ideally, this config should be fetched from db too, for now it's here - /** - * tables which should always be left joined to because they might not have a mapping - */ - static sparseTables: string[] = ["tinx_novelty"]; - - /** - * tables which have a data type column which describes the data the caller wants - * These should be left joined to columns matching caller's datatype - */ - static typeTables: string[] = ["tdl_info"]; - static typeTableColumns: Map = new Map( - [ - ["tdl_info", "itype"] - ] - ); - static preferredLink: Map = new Map( [ ["ncats_ppi-protein", "protein_id"] ] ); - static additionalWhereClause(table: string, alias: string, dataModelListObj: DataModelList) { - if (table == "ncats_ppi") { - return `${alias}.other_id = (select id from protein where match(uniprot,sym,stringid) against('${dataModelListObj.associatedTarget}' in boolean mode)) - and NOT (${alias}.ppitypes = 'STRINGDB' AND ${alias}.score < ${dataModelListObj.ppiConfidence})`; - } - return null; - } - - static typeTableColumnMapping: Map = new Map( - [ - ["tdl_info-Ab Count", "integer_value"], - ["tdl_info-MAb Count", "integer_value"], - ["tdl_info-NCBI Gene PubMed Count", "integer_value"], - ["tdl_info-EBI Total Patent Count", "integer_value"], - ["tdl_info-ChEMBL First Reference Year", "integer_value"], - - ["tdl_info-JensenLab PubMed Score", "number_value"], - ["tdl_info-PubTator Score", "number_value"], - ["tdl_info-HPM Protein Tissue Specificity Index", "number_value"], - ["tdl_info-HPM Gene Tissue Specificity Index", "number_value"], - ["tdl_info-HPA Tissue Specificity Index", "number_value"], - - ["tdl_info-IMPC Clones", "string_value"], - ["tdl_info-TMHMM Prediction", "string_value"], - ["tdl_info-UniProt Function", "string_value"], - ["tdl_info-ChEMBL Selective Compound", "string_value"], - ["tdl_info-Experimental MF/BP Leaf Term GOA", "string_value"], - ["tdl_info-Antibodypedia.com URL", "string_value"], - ["tdl_info-IMPC Status", "string_value"], - ["tdl_info-NCBI Gene Summary", "string_value"], - - ["tdl_info-Is Transcription Factor", "boolean_value"] - ] - ); - - static leftJoinTables: string[] = DatabaseTable.sparseTables.concat(DatabaseTable.typeTables); - static requiredLinks: Map = new Map( [ - ["protein-viral_protein", ["viral_ppi", "virus"]], - ["protein-virus", ["viral_ppi", "viral_protein"]], + ["protein-viral_protein", ["viral_ppi"]], + // checked + ["protein-virus", ["viral_protein", "viral_ppi"]], ["protein-dto", ["p2dto"]], ["protein-panther_class", ["p2pc"]], - ["protein-virus", ["viral_protein", "viral_ppi"]], - ["protein-viral_protein", ["virus", "viral_ppi"]], - - // checked ["protein-target", ["t2tc"]], + ["protein-ncats_idg_list_type", ["ncats_idg_list"]], ["protein-ncats_ligands", ["t2tc", "target", "ncats_ligand_activity"]], ["protein-ncats_ligand_activity", ["t2tc", "target"]] ]); static getRequiredLinks(table1: string, table2: string): string[] | undefined { - const reqTables = DatabaseTable.requiredLinks.get(table1 + "-" + table2); + let reqTables = DatabaseTable.requiredLinks.get(table1 + "-" + table2); if (reqTables) return reqTables; - return DatabaseTable.requiredLinks.get(table2 + "-" + table1); + return DatabaseTable.requiredLinks.get(table2 + "-" + table1)?.slice().reverse(); } } diff --git a/src/models/disease/diseaseFacetFactory.ts b/src/models/disease/diseaseFacetFactory.ts index c5a069c..c4298a1 100644 --- a/src/models/disease/diseaseFacetFactory.ts +++ b/src/models/disease/diseaseFacetFactory.ts @@ -1,27 +1,23 @@ import {FacetFactory} from "../FacetFactory"; import {DataModelList} from "../DataModelList"; -import {FacetInfo} from "../FacetInfo"; +import {FieldInfo} from "../FieldInfo"; export class DiseaseFacetFactory extends FacetFactory { - GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FacetInfo { - const facet_config = parent.databaseConfig.getFacetConfig(parent.rootTable, typeName); - const partialReturn = this.parse_facet_config(parent, typeName, allowedValues, false, facet_config); + GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FieldInfo { + const fieldInfo = parent.databaseConfig.getOneField('Disease', 'facet', parent.getAssociatedModel(), '', typeName); + if(!fieldInfo){ + return this.unknownFacet(); + } switch (typeName) { case "Data Source": - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedTarget, - whereClause: this.getdiseaseWhereClause(parent.associatedTarget) - } as FacetInfo); + fieldInfo.typeModifier = parent.associatedTarget; + fieldInfo.where_clause = this.getdiseaseWhereClause(parent.associatedTarget) || fieldInfo.where_clause; case "Drug": - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedTarget, - whereClause: this.getdiseaseWhereClause(parent.associatedTarget, "drug_name is not null") - } as FacetInfo); + fieldInfo.typeModifier = parent.associatedTarget; + fieldInfo.where_clause = this.getdiseaseWhereClause(parent.associatedTarget, "drug_name is not null") || fieldInfo.where_clause; } - return new FacetInfo(partialReturn); + return fieldInfo; } getdiseaseWhereClause(sym: string, extraClause?: string) { diff --git a/src/models/disease/diseaseList.ts b/src/models/disease/diseaseList.ts index e28705f..c63da6a 100644 --- a/src/models/disease/diseaseList.ts +++ b/src/models/disease/diseaseList.ts @@ -1,5 +1,5 @@ import {DataModelList} from "../DataModelList"; -import {FacetInfo} from "../FacetInfo"; +import {FieldInfo} from "../FieldInfo"; import {DiseaseFacetFactory} from "./diseaseFacetFactory"; import {ConfigKeys} from "../config"; @@ -58,34 +58,24 @@ export class DiseaseList extends DataModelList { } else { facetList = this.DefaultFacets; } - this.facetsToFetch = FacetInfo.deduplicate( + this.facetsToFetch = FieldInfo.deduplicate( this.facetsToFetch.concat(this.facetFactory.getFacetsFromList(this, facetList))); } get DefaultFacetsWithTarget() { - return this.databaseConfig.fieldLists - .get('Disease Facet - Associated Target')?.sort((a,b) => a.order - b.order) - .map(a => a.type) || []; + return this.databaseConfig.getDefaultFields('Disease', 'facet', 'Target') + .map(a => a.name) || []; }; defaultSortParameters(): {column: string; order: string}[] { return [{column: 'count', order: 'desc'}] }; - listQueryKey() { - return ConfigKeys.Disease_List_Default + getDefaultFields() : FieldInfo[] { + return []; + // return ConfigKeys.Disease_List_Default }; - addLinkToRootTable(query: any, facet: FacetInfo): void { - if (facet.dataTable === 'target') { - query.andWhere('target.id', this.database.raw('t2tc.target_id')) - .andWhere('disease.protein_id', this.database.raw('t2tc.protein_id')); - } - if (facet.dataTable === 'ncats_dataSource_map'){ - query.andWhere('disease.ncats_name', this.database.raw('ncats_dataSource_map.disease_name')); - } - } - addModelSpecificFiltering(query: any): void { if (this.associatedTarget) { query.join(this.getAssociatedTargetQuery().as('assocTarget'), 'assocTarget.name', this.keyString()); @@ -104,20 +94,8 @@ export class DiseaseList extends DataModelList { .orderBy("associationCount", "desc"); } - getRequiredTablesForFacet(info: FacetInfo): string[] { - let tableList = []; - tableList.push(this.rootTable); - if (info.dataTable == this.rootTable) { - return tableList; - } - tableList.push(info.dataTable); - switch (info.dataTable) { - case "target": - tableList.push("t2tc"); - break; - } - - return tableList; + getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string { + return ''; } } diff --git a/src/models/ligand/ligandFacetFactory.ts b/src/models/ligand/ligandFacetFactory.ts index 62b7a26..85ded97 100644 --- a/src/models/ligand/ligandFacetFactory.ts +++ b/src/models/ligand/ligandFacetFactory.ts @@ -1,26 +1,21 @@ import {FacetFactory} from "../FacetFactory"; import {DataModelList} from "../DataModelList"; -import {FacetInfo} from "../FacetInfo"; +import {FieldInfo} from "../FieldInfo"; export class LigandFacetFactory extends FacetFactory { - GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FacetInfo { - const facet_config = parent.databaseConfig.getFacetConfig(parent.rootTable, typeName); - const partialReturn = this.parse_facet_config(parent, typeName, allowedValues, false, facet_config); + GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FieldInfo { + const fieldInfo = parent.databaseConfig.getOneField('Disease', 'facet', parent.getAssociatedModel(), '', typeName); + if(!fieldInfo){ + return this.unknownFacet(); + } switch (typeName) { case "Activity": - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedTarget, - whereClause: this.getActivityWhereClause(parent.associatedTarget, "act_type not in ('','-')") - } as FacetInfo); + fieldInfo.typeModifier = parent.associatedTarget; + fieldInfo.where_clause = this.getActivityWhereClause(parent.associatedTarget, "act_type not in ('','-')") || fieldInfo.where_clause; case "Action": - return new FacetInfo( - { - ...partialReturn, - typeModifier: parent.associatedTarget, - whereClause: this.getActivityWhereClause(parent.associatedTarget, "action_type is not null") - } as FacetInfo); + fieldInfo.typeModifier = parent.associatedTarget; + fieldInfo.where_clause = this.getActivityWhereClause(parent.associatedTarget, "action_type is not null") || fieldInfo.where_clause; case "EC50": case "IC50": case "Kd": @@ -28,14 +23,10 @@ export class LigandFacetFactory extends FacetFactory { if (!parent.associatedTarget) { return this.unknownFacet(); } - return new FacetInfo( - { - ...partialReturn, - typeModifier: parent.associatedTarget, - whereClause: this.getActivityWhereClause(parent.associatedTarget, `act_type = '${typeName}'`) - } as FacetInfo); + fieldInfo.typeModifier = parent.associatedTarget; + fieldInfo.where_clause = this.getActivityWhereClause(parent.associatedTarget, `act_type = '${typeName}'`) || fieldInfo.where_clause; } - return new FacetInfo(partialReturn); + return fieldInfo; } getActivityWhereClause(sym: string, extraClause?: string){ diff --git a/src/models/ligand/ligandList.ts b/src/models/ligand/ligandList.ts index 2b7b758..5cb5c33 100644 --- a/src/models/ligand/ligandList.ts +++ b/src/models/ligand/ligandList.ts @@ -1,5 +1,5 @@ import {DataModelList} from "../DataModelList"; -import {FacetInfo} from "../FacetInfo"; +import {FieldInfo} from "../FieldInfo"; import {LigandFacetFactory} from "./ligandFacetFactory"; import {ConfigKeys} from "../config"; @@ -25,14 +25,13 @@ export class LigandList extends DataModelList{ } else { facetList = this.DefaultFacets; } - this.facetsToFetch = FacetInfo.deduplicate( + this.facetsToFetch = FieldInfo.deduplicate( this.facetsToFetch.concat(this.facetFactory.getFacetsFromList(this, facetList))); } get DefaultFacetsWithTarget() { - return this.databaseConfig.fieldLists - .get('Ligand Facet - Associated Target')?.sort((a,b) => a.order - b.order) - .map(a => a.type) || []; + return this.databaseConfig.getDefaultFields('Ligand', 'facet', 'Target') + .map(a => a.name) || []; }; defaultSortParameters(): {column: string; order: string}[] @@ -40,11 +39,10 @@ export class LigandList extends DataModelList{ return [{column: 'actcnt', order: 'desc'}]; }; - listQueryKey() {return ConfigKeys.Ligand_List_Default}; - - addLinkToRootTable(query: any, facet: FacetInfo): void { - query.andWhere('ncats_ligands.id', this.database.raw(facet.dataTable + '.ncats_ligand_id')); - } + getDefaultFields(): FieldInfo[] { + return []; + // return ConfigKeys.Ligand_List_Default + }; addModelSpecificFiltering(query: any): void { if(this.associatedTarget){ @@ -61,14 +59,8 @@ export class LigandList extends DataModelList{ } } - getRequiredTablesForFacet(info: FacetInfo): string[] { - let tableList = []; - tableList.push(this.rootTable); - if (info.dataTable == this.rootTable) { - return tableList; - } - tableList.push(info.dataTable); - return tableList; + getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string { + return ''; } } diff --git a/src/models/queryDefinition.ts b/src/models/queryDefinition.ts new file mode 100644 index 0000000..299e278 --- /dev/null +++ b/src/models/queryDefinition.ts @@ -0,0 +1,170 @@ +import {FieldInfo} from "./FieldInfo"; +import {DatabaseTable} from "./databaseTable"; +import {SqlTable} from "./sqlTable"; +import {IBuildable} from "./IBuildable"; + +/** + * Class to hold all the details required to generate a query given a required set of data + */ +export class QueryDefinition { + buildable: IBuildable; + static GenerateQueryDefinition(buildabelObj: IBuildable, dataList: FieldInfo[]): QueryDefinition { + let qd = new QueryDefinition(buildabelObj); + for (let i = 0; i < dataList.length; i++) { + qd.addRequestedData(dataList[i]); + } + return qd; + } + + tables: SqlTable[] = []; + + private constructor(buildableObj: IBuildable) { + this.buildable = buildableObj; + } + + addRequestedData(reqData: FieldInfo) { + reqData.where_clause = this.buildable.getSpecialModelWhereClause(reqData, this.buildable.rootTable) || reqData.where_clause; + const existingTable = this.tables.find(table => { + return table.equals(reqData.table, reqData.where_clause); + }); + if (existingTable) { + existingTable.columns.push(reqData); + return; + } + this.addRequestedDataToNewTable(reqData); + } + + addRequestedDataToNewTable(reqData: FieldInfo) { + let links: string[] = []; + const tableCount = this.tables.filter(t => { + return t.tableName == reqData.table; + }).length; + + let tableAlias = reqData.table; + if(tableCount > 0){ + tableAlias = reqData.table + tableCount; + const re = new RegExp('\\b' + reqData.table + '\\b', 'i'); + reqData.where_clause = reqData.where_clause?.replace(re, tableAlias); + reqData.select = reqData.select?.replace(re, tableAlias); + } + + if (reqData.table != this.buildable.rootTable) { + links = DatabaseTable.getRequiredLinks(reqData.table, this.buildable.rootTable) || []; + } + + + const newTable = new SqlTable(reqData.table, { + alias: tableAlias, + joinConstraint: reqData.where_clause + }, links); + newTable.columns.push(reqData); + this.tables.push(newTable); + } + + + getJoinTables(): SqlTable[] { + return this.tables.filter(table => !this.isRootTable(table)); + } + + + hasGroupedColumns(): boolean { + let found = false; + this.tables.forEach(table => { + table.columns.forEach(column => { + if (column.group_method) { + found = true; + } + }); + }); + return found; + } + + + getSqlTable(field: FieldInfo): SqlTable { + let sortTable: SqlTable; + this.tables.forEach(t => { + if(!sortTable) { + t.columns.forEach(c => { + if(c.name === field.name && c.table === field.table && c.column === field.column) { + sortTable = t; + } + }); + } + }); + // @ts-ignore + return sortTable; + } + + getColumnList(db: any) { + const columnList: any = {}; + this.tables.forEach(table => { + table.columns.forEach(column => { + const select = column.select || `${table.alias}.${column.column}`; + const columnName = column.alias || column.column; + if (column.group_method) { + columnList[columnName] = db.raw(column.group_method + `(distinct ${select})`); + } else if (column.needsDistinct){ + columnList[columnName] = db.raw(`distinct ${select}`); + } else { + columnList[columnName] = db.raw(select); + } + }) + }); + return columnList; + } + + getRootTable(): SqlTable { + const rootTable = this.tables.find(table => { + return this.isRootTable(table); + }); + if (rootTable) { + return rootTable; + } + return new SqlTable(this.buildable.rootTable); + } + + isRootTable(dataTable: SqlTable): boolean { + return dataTable.tableName === this.buildable.rootTable && dataTable.alias === this.buildable.rootTable; + } + + generateBaseQuery(forFacet: boolean){ + const buildableObj = this.buildable; + let rootTableObject = this.getRootTable(); + if (rootTableObject == undefined) { + throw new Error('failed to build SQL query'); + } + let joinTables = this.getJoinTables(); + let query = buildableObj.database(rootTableObject.tableName) + .select(this.getColumnList(buildableObj.database)); + + let joinFunction = 'leftJoin'; + if(forFacet){ + joinFunction = 'join'; + } + + joinTables.forEach(dataTable => { + if(dataTable.columns[0].isFromListQuery) { + return; + } + let leftTable = rootTableObject; + dataTable.linkingTables.forEach(linkTableName => { + let linkInfo = buildableObj.databaseConfig.getLinkInformation(leftTable.tableName, linkTableName); + // @ts-ignore + query[joinFunction](linkTableName, function (this: any) { + this.on(`${leftTable.alias}.${linkInfo.fromCol}`, `=`, `${linkTableName}.${linkInfo.toCol}`); + }); + leftTable = new SqlTable(linkTableName); + }); + let linkInfo = buildableObj.databaseConfig.getLinkInformation(leftTable.tableName, dataTable.tableName); + + // @ts-ignore + query[joinFunction]({[dataTable.alias]: dataTable.tableName}, function (this: any) { + this.on(`${leftTable.alias}.${linkInfo.fromCol}`, `=`, `${dataTable.alias}.${linkInfo.toCol}`); + if(dataTable.joinConstraint){ + this.andOn(buildableObj.database.raw(dataTable.joinConstraint)); + } + }); + }); + return query; + } +} diff --git a/src/models/similarTargets/jaccard.ts b/src/models/similarTargets/jaccard.ts index a322d9f..fbcd7ad 100644 --- a/src/models/similarTargets/jaccard.ts +++ b/src/models/similarTargets/jaccard.ts @@ -1,14 +1,12 @@ import {DatabaseConfig} from "../databaseConfig"; -import {FacetInfo} from "../FacetInfo"; -import {DatabaseTable} from "../databaseTable"; +import {FieldInfo} from "../FieldInfo"; export class Jaccard{ - knex: any; dbConfig: DatabaseConfig; match: string; - facet: FacetInfo; + facet: FieldInfo; rootTable: string; matchQuery: string; keyString: string; @@ -21,11 +19,13 @@ export class Jaccard{ this.rootTable = rootTable; const rootTableObj = this.dbConfig.tables.find(table => table.tableName === this.rootTable); this.keyString = `${rootTableObj?.tableName}.${rootTableObj?.primaryKey}`; - this.facet = new FacetInfo(this.dbConfig.getFacetConfig(this.rootTable ,similarity.facet)); + + // @ts-ignore + this.facet = this.dbConfig.getOneField('Target', 'overlap', '', '', similarity.facet); } getListQuery(forList: boolean) { - if(!this.facet.dataTable) { + if(!this.facet.table) { return null;} const matchCountQuery = this.getCountForMatch(); const testCountQuery = this.getCountForTest(); @@ -77,8 +77,4 @@ export class Jaccard{ const testSetQuery = this.dbConfig.getBaseSetQuery(this.rootTable, this.facet); return testSetQuery.where(this.knex.raw(`${this.keyString} = testID`)); } - - - - } diff --git a/src/models/sqlTable.ts b/src/models/sqlTable.ts new file mode 100644 index 0000000..fb9343a --- /dev/null +++ b/src/models/sqlTable.ts @@ -0,0 +1,35 @@ +import {FieldInfo} from "./FieldInfo"; + +export class SqlTable { + tableName: string; + private _alias?: string; + get alias(): string { + if (this._alias) return this._alias; + return this.tableName; + } + + joinConstraint: string; + columns: FieldInfo[] = []; + linkingTables: string[] = []; + + constructor(tableName: string, {alias = "", joinConstraint = ""} = {}, + linkingTables: string[] = []) { + if (alias) { + this._alias = alias; + } + this.tableName = tableName; + this.joinConstraint = joinConstraint; + + this.linkingTables = linkingTables; + if (alias) { + this._alias = alias; + } + } + + equals(tableName: string, joinConstraint: string | undefined) { + if (!!this.joinConstraint || !!joinConstraint) { + return this.tableName === tableName && this.joinConstraint === joinConstraint; + } + return this.tableName === tableName; + } +} diff --git a/src/models/target/targetDetails.ts b/src/models/target/targetDetails.ts index b4b8f61..376304b 100644 --- a/src/models/target/targetDetails.ts +++ b/src/models/target/targetDetails.ts @@ -1,12 +1,12 @@ import {DatabaseConfig} from "../databaseConfig"; -import {FacetInfo} from "../FacetInfo"; +import {FieldInfo} from "../FieldInfo"; export class TargetDetails{ facetName: string; knex: any; databaseConfig: DatabaseConfig; - facet: FacetInfo; + facet: FieldInfo; target: any; top: number; skip: number; @@ -17,7 +17,9 @@ export class TargetDetails{ this.databaseConfig = tcrd.tableInfo; this.top = args.top || 10; this.skip = args.skip || 0; - this.facet = new FacetInfo(this.databaseConfig.getFacetConfig('protein', this.facetName)); + + // @ts-ignore + this.facet = this.databaseConfig.getOneField('Target', 'facet', '', '', this.facetName); } getFacetValueCount(){ diff --git a/src/models/target/targetFacetFactory.ts b/src/models/target/targetFacetFactory.ts index debb506..54a5c10 100644 --- a/src/models/target/targetFacetFactory.ts +++ b/src/models/target/targetFacetFactory.ts @@ -1,79 +1,19 @@ import {DataModelList} from "../DataModelList"; -import {FacetInfo} from "../FacetInfo"; +import {FieldInfo} from "../FieldInfo"; import {FacetFactory} from "../FacetFactory"; -import {DiseaseList} from "../disease/diseaseList"; -export class TargetFacetFactory extends FacetFactory{ +export class TargetFacetFactory extends FacetFactory { - GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], nullQuery: boolean = false): FacetInfo { - const facet_config = parent.databaseConfig.getFacetConfig(parent.rootTable, typeName); - const partialReturn = this.parse_facet_config(parent, typeName, allowedValues, nullQuery, facet_config); - - switch (typeName) { - case "PPI Data Source": - if(!parent.associatedTarget) { return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedTarget, - whereClause: `other_id = (select id from protein where match(uniprot,sym,stringid) against('${parent.associatedTarget}' in boolean mode))`, - valuesDelimited: true - } as FacetInfo); - case "Disease Data Source": - if(!parent.associatedDisease) {return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedDisease, - whereClause: `disease.ncats_name in (${DiseaseList.getDescendentsQuery(parent.database, parent.associatedDisease).toString()})`, - } as FacetInfo); - case "Linked Disease": - if(!parent.associatedDisease) {return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - whereClause: `disease.ncats_name in (${DiseaseList.getDescendentsQuery(parent.database, parent.associatedDisease).toString()})`, - } as FacetInfo); - case "StringDB Interaction Score": - if(!parent.associatedTarget) { return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedTarget, - whereClause: `other_id = (select id from protein where match(uniprot,sym,stringid) against('${parent.associatedTarget}' in boolean mode))` - } as FacetInfo); - case "BioPlex Interaction Probability": - if(!parent.associatedTarget) { return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedTarget, - whereClause: `other_id = (select id from protein where match(uniprot,sym,stringid) against('${parent.associatedTarget}' in boolean mode))` - } as FacetInfo); - case "JensenLab TextMining zscore": - if(!parent.associatedDisease) {return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedDisease, - whereClause: `disease.ncats_name in (${DiseaseList.getDescendentsQuery(parent.database, parent.associatedDisease).toString()})` - } as FacetInfo); - case "JensenLab Confidence": - if(!parent.associatedDisease) {return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedDisease, - whereClause: `disease.ncats_name in (${DiseaseList.getDescendentsQuery(parent.database, parent.associatedDisease).toString()})` - } as FacetInfo); - case "Expression Atlas Log2 Fold Change": - if(!parent.associatedDisease) {return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedDisease, - whereClause: `disease.ncats_name in (${DiseaseList.getDescendentsQuery(parent.database, parent.associatedDisease).toString()})` - } as FacetInfo); - case "DisGeNET Score": - if(!parent.associatedDisease) {return this.unknownFacet();} - return new FacetInfo({ - ...partialReturn, - typeModifier: parent.associatedDisease, - whereClause: `disease.ncats_name in (${DiseaseList.getDescendentsQuery(parent.database, parent.associatedDisease).toString()})` - } as FacetInfo); + GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], nullQuery: boolean = false): FieldInfo { + const fieldInfo = parent.databaseConfig.getOneField('Target', 'facet', parent.getAssociatedModel(), '', typeName); + if(!fieldInfo){ + return this.unknownFacet(); + } + fieldInfo.allowedValues = allowedValues; + fieldInfo.parent = parent; + if (typeName === "PPI Data Source") { // :( + fieldInfo.valuesDelimited = true; } - return new FacetInfo(partialReturn); + return fieldInfo; } } diff --git a/src/models/target/targetList.ts b/src/models/target/targetList.ts index a0259c4..0adb95d 100644 --- a/src/models/target/targetList.ts +++ b/src/models/target/targetList.ts @@ -1,50 +1,43 @@ import {TargetFacetFactory} from "./targetFacetFactory"; import {DataModelList} from "../DataModelList"; -import {FacetInfo} from "../FacetInfo"; -import {ConfigKeys} from "../config"; +import {FieldInfo} from "../FieldInfo"; import {Jaccard} from "../similarTargets/jaccard"; export class TargetList extends DataModelList { - batch: string[] = []; skip: number = 0; top: number = 10; proteinList: string[] = []; proteinListCached: boolean = false; - defaultSortParameters(): {column: string; order: string}[] { - if(this.fields.length > 0){ + defaultSortParameters(): { column: string; order: string }[] { + if (this.fields.length > 0) { return [{column: 'id', order: 'asc'}]; } - if(this.term){ - return [{column:'min_score', order:'asc'},{column:'name', order:'asc'}]; + if (this.term) { + return [{column: 'min_score', order: 'asc'}, {column: 'name', order: 'asc'}]; } - if(this.associatedTarget){ - return [{column:'p_int', order:'desc'},{column:'score', order:'desc'}]; + if (this.associatedTarget) { + return [{column: 'p_int', order: 'desc'}, {column: 'score', order: 'desc'}]; } - if(this.associatedDisease){ - return [{column: 'dtype', order:'desc'}]; + if (this.associatedDisease) { + return [{column: 'dtype', order: 'desc'}]; } - if(this.similarity.match.length > 0){ + if (this.similarity.match.length > 0) { return [{column: 'jaccard', order: 'desc'}]; } - return [{column:'novelty', order:'desc'}]; + return [{column: 'novelty', order: 'desc'}]; } constructor(tcrd: any, json: any) { - super(tcrd,"protein", "id", new TargetFacetFactory(), json); - if (json && json.batch) { - this.batch = json.batch; - } + super(tcrd, "protein", "id", new TargetFacetFactory(), json); let facetList: string[]; if (!json || !json.facets || json.facets.length == 0) { - if(this.associatedTarget){ + if (this.associatedTarget) { facetList = this.DefaultPPIFacets; - } - else if(this.associatedDisease){ + } else if (this.associatedDisease) { facetList = this.DefaultDiseaseFacets; - } - else { + } else { facetList = this.DefaultFacets; } } else if (json.facets == "all") { @@ -52,7 +45,7 @@ export class TargetList extends DataModelList { } else { facetList = json.facets; } - this.facetsToFetch = FacetInfo.deduplicate( + this.facetsToFetch = FieldInfo.deduplicate( this.facetsToFetch.concat(this.facetFactory.getFacetsFromList(this, facetList, this.isNull()))); if (json) { @@ -61,99 +54,66 @@ export class TargetList extends DataModelList { } } - listQueryKey() { - if(this.associatedTarget){ - return ConfigKeys.Target_List_PPI; + getDefaultFields(): FieldInfo[] { + if (this.associatedTarget) { + return this.databaseConfig.getAvailableFields('Target', 'list', 'Target'); + } + if (this.associatedDisease) { + return this.databaseConfig.getAvailableFields('Target', 'list', 'Disease'); } - if(this.associatedDisease){ - return ConfigKeys.Target_List_Disease; + if (this.similarity.match.length > 0) { + const dataFields = this.databaseConfig.getAvailableFields('Target', 'list', '', 'Similarity'); + dataFields.push({isFromListQuery: true, table: "filterQuery", column: "overlap"} as FieldInfo); + dataFields.push({isFromListQuery: true, table: "filterQuery", column: "baseSize"} as FieldInfo); + dataFields.push({isFromListQuery: true, table: "filterQuery", column: "testSize"} as FieldInfo); + dataFields.push({isFromListQuery: true, table: "filterQuery", column: "commonOptions"} as FieldInfo); + dataFields.push({isFromListQuery: true, table: "filterQuery", column: "jaccard"} as FieldInfo); + return dataFields; } - if(this.similarity.match.length > 0){ - return ConfigKeys.Target_List_Similarity; - } - return ConfigKeys.Target_List_Default; + const dataFields = this.databaseConfig.getAvailableFields('Target', 'list'); + if(this.term.length > 0){ + dataFields.push({isFromListQuery: true, table: 'filterQuery', column: 'min_score', alias: 'search_score'} as FieldInfo); + } + return dataFields; } addModelSpecificFiltering(query: any, list: boolean = false): void { - if (list && this.term.length > 0) { - query.join(this.tcrd.getScoredProteinList(this.term).as("searchQuery"), 'searchQuery.protein_id', 'protein.id'); - } else if (list && this.similarity.match.length > 0) { - const subq = new Jaccard( - { - ...this.similarity, - matchQuery: `match(protein.uniprot,protein.sym,protein.stringid) against('${this.similarity.match}' in boolean mode)` - }, - this.rootTable, this.database, this.databaseConfig).getListQuery(true); - if(subq) { - query.join(subq.as("similarityQuery"), 'similarityQuery.protein_id', 'protein.id'); + let filterQuery; + if (list) { + if(this.term.length > 0){ + filterQuery = this.tcrd.getScoredProteinList(this.term); + } + else if(this.similarity.match.length > 0) { + filterQuery = this.getSimilarityQuery(); + } + else if(this.associatedDisease.length > 0) { + filterQuery = this.fetchProteinList(); + } + else if(this.associatedTarget.length > 0) { + filterQuery = this.fetchProteinList(); } } else { - if(!list || !this.associatedTarget || (this.fields.length > 0)) { - this.addProteinListConstraint(query, this.fetchProteinList()); + filterQuery = this.fetchProteinList(); + } + if(!!filterQuery) { + if (Array.isArray(filterQuery)) { // cached protein list + query.whereIn('protein.id', filterQuery); + } else { + query.join(filterQuery.as("filterQuery"), 'filterQuery.protein_id', 'protein.id'); } } - this.addBatchConstraint(query, this.batch); - } - - addLinkToRootTable(query: any, facet: FacetInfo): void { // TODO, use database.ts instead, maybe we don't need this at all - if (facet.dataTable == 'target') { - query.andWhere('target.id', this.database.raw('t2tc.target_id')) - .andWhere('protein.id', this.database.raw('t2tc.protein_id')); - } else if (facet.dataTable == 'ncats_idg_list_type') { - query.andWhere('protein.id', this.database.raw('ncats_idg_list.protein_id')) - .andWhere('ncats_idg_list_type.id', this.database.raw('ncats_idg_list.idg_list')); - } else if (facet.type == "IMPC Phenotype") { - query.andWhere('ortholog.geneid', this.database.raw('nhprotein.geneid')) - .andWhere('ortholog.taxid', this.database.raw('nhprotein.taxid')) - .andWhere('nhprotein.id', this.database.raw('phenotype.nhprotein_id')) - .andWhere('protein.id', this.database.raw('ortholog.protein_id')); - } else if (facet.dataTable === 'viral_protein' || facet.dataTable === 'virus') { - query.andWhere('protein.id', this.database.raw('viral_ppi.protein_id')) - .andWhere('viral_ppi.viral_protein_id', this.database.raw('viral_protein.id')) - .andWhere('virus.virusTaxid', this.database.raw('viral_protein.virus_id')); - }else if (facet.dataTable === 'panther_class') { - query.andWhere('protein.id', this.database.raw('p2pc.protein_id')) - .andWhere('p2pc.panther_class_id', this.database.raw('panther_class.id')); - }else if (facet.dataTable === 'dto') { - query.andWhere('protein.id', this.database.raw('p2dto.protein_id')) - .andWhere('p2dto.dtoid', this.database.raw('dto.dtoid')); - } else { // default is to use protein_id column from keyTable - query.andWhere('protein.id', this.database.raw(facet.dataTable + '.protein_id')); + if (this.batch && this.batch.length > 0) { + query.join(this.getBatchQuery(this.batch).as('batchQuery'), 'batchQuery.protein_id', 'protein.id'); } } - getRequiredTablesForFacet(info: FacetInfo): string[] { // TODO this too - let tableList = []; - tableList.push(this.rootTable); - if (info.dataTable == this.rootTable) { - return tableList; - } - tableList.push(info.dataTable); - switch (info.dataTable) { - case "target": - tableList.push("t2tc"); - break; - case "ncats_idg_list_type": - tableList.push("ncats_idg_list"); - break; - case "viral_protein": - case "virus": - tableList.push("viral_ppi"); - tableList.push("viral_protein"); - tableList.push("virus"); - break; - case "panther_class": - tableList.push("p2pc"); - break; - case "dto": - tableList.push("p2dto"); - break; - } - if (info.type == "IMPC Phenotype") { - tableList.push("nhprotein"); - tableList.push("ortholog"); - } - return tableList; + private getSimilarityQuery() { + return new Jaccard( + { + ...this.similarity, + matchQuery: `match(protein.uniprot,protein.sym,protein.stringid) against('${this.similarity.match}' in boolean mode)` + }, + this.rootTable, this.database, this.databaseConfig).getListQuery(true); } fetchProteinList(): any { @@ -168,20 +128,21 @@ export class TargetList extends DataModelList { proteinListQuery = this.tcrd.getProteinList(this.term); } else if (this.associatedTarget) { proteinListQuery = this.tcrd.getProteinListFromPPI(this.associatedTarget, this.ppiConfidence); - } - else if (this.similarity.match.length > 0){ + } else if (this.similarity.match.length > 0) { proteinListQuery = new Jaccard( - {...this.similarity, matchQuery: `match(protein.uniprot,protein.sym,protein.stringid) against('${this.similarity.match}' in boolean mode)`}, + { + ...this.similarity, + matchQuery: `match(protein.uniprot,protein.sym,protein.stringid) against('${this.similarity.match}' in boolean mode)` + }, this.rootTable, this.database, this.databaseConfig).getListQuery(false); - } - else{ + } else { proteinListQuery = this.getDiseaseQuery(); } this.captureQueryPerformance(proteinListQuery, "protein list"); return proteinListQuery; } - getDiseaseQuery(){ + getDiseaseQuery() { return this.database.select(this.database.raw(` distinct protein_id FROM @@ -207,51 +168,30 @@ ON diseaseList.name = d.ncats_name`)); this.proteinList = list; } - addBatchConstraint(query: any, batch: string[]) { - if (!!batch && batch.length > 0) { - query.andWhere - (function (builder: any) { - builder.whereIn('protein.uniprot', batch) - .orWhereIn('protein.sym', batch) - .orWhereIn('protein.stringid', batch) - }); - } - }; - - addProteinListConstraint(query: any, proteinList: any) { - if (!!proteinList) { - query.whereIn('protein.id', proteinList); - } - }; - - isNull() { - if (this.batch.length > 0) { - return false; - } - if (this.term.length > 0) { - return false; - } - if(this.associatedTarget.length > 0) { - return false; - } - if(this.associatedDisease.length > 0){ - return false; - } - if (this.filteringFacets.length > 0) { - return false; - } - return true; + getBatchQuery(batch: string[]){ + return this.database('protein').distinct({protein_id: 'id'}) + .whereIn('protein.uniprot', batch) + .orWhereIn('protein.sym', batch) + .orWhereIn('protein.stringid', batch); } get DefaultPPIFacets() { - return this.databaseConfig.fieldLists - .get('Target Facet - Associated Target')?.sort((a,b) => a.order - b.order) - .map(a => a.type) || []; + return this.databaseConfig.getDefaultFields('Target', 'facet', 'Target') + .sort((a, b) => a.order - b.order) + .map(a => a.name) || []; }; get DefaultDiseaseFacets() { - return this.databaseConfig.fieldLists - .get('Target Facet - Associated Disease')?.sort((a,b) => a.order - b.order) - .map(a => a.type) || []; + return this.databaseConfig.getDefaultFields('Target', 'facet', 'Disease') + .sort((a, b) => a.order - b.order) + .map(a => a.name) || []; }; + + getSpecialModelWhereClause( fieldInfo: FieldInfo, rootTableOverride: string): string { + if (this.associatedTarget && (fieldInfo.table === 'ncats_ppi' || rootTableOverride === 'ncats_ppi')) { + return `ncats_ppi.other_id = (select id from protein where match(uniprot,sym,stringid) against('${this.associatedTarget}' in boolean mode)) + and NOT (ncats_ppi.ppitypes = 'STRINGDB' AND ncats_ppi.score < ${this.ppiConfidence})`; + } + return ""; + } } diff --git a/src/resolvers.js b/src/resolvers.js index 5b5f649..8ebebc5 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -1,6 +1,6 @@ const {DataModelListFactory} = require("./models/DataModelListFactory"); const {TargetDetails} = require("./models/target/targetDetails"); -const {FacetDataType} = require("./models/FacetInfo"); +const {FacetDataType} = require("./models/FieldInfo"); const {LigandList} = require("./models/ligand/ligandList"); const {DiseaseList} = require("./models/disease/diseaseList"); const {TargetList} = require("./models/target/targetList"); @@ -119,7 +119,7 @@ const resolvers = { }, targetFacets: async function (_, args, {dataSources}) { - return dataSources.tcrd.tableInfo.fieldLists.get('Target Facet - All').map(facet => facet.type); + return dataSources.tcrd.tableInfo.fieldLists.get('Target Facet - All').map(facet => facet.name); }, target: async function (_, args, {dataSources}) { @@ -249,14 +249,14 @@ const resolvers = { batch: async function (line, args, {dataSources}, info) { let funcs = [ getTargetResult(args, dataSources), - getDiseaseResult(args, dataSources.tcrd), - getLigandResult(args, dataSources.tcrd) + // getDiseaseResult(args, dataSources.tcrd), + // getLigandResult(args, dataSources.tcrd) ]; return Promise.all(funcs).then(r => { return { targetResult: r[0], - diseaseResult: r[1], - ligandResult: r[2] // sigh + // diseaseResult: r[1], + // ligandResult: r[2] // sigh }; }).catch(function (error) { console.error(error); @@ -1608,13 +1608,13 @@ function getTargetResult(args, dataSources) { facets.push({ dataType: targetList.facetsToFetch[i].dataType == FacetDataType.numeric ? "Numeric" : "Category", binSize: targetList.facetsToFetch[i].binSize, - facet: targetList.facetsToFetch[i].type, + facet: targetList.facetsToFetch[i].name, modifier: targetList.facetsToFetch[i].typeModifier, count: rowData.length, values: rowData, sql: facetQueries[i].toString(), - elapsedTime: targetList.getElapsedTime(targetList.facetsToFetch[i].type), - sourceExplanation: targetList.facetsToFetch[i].sourceExplanation + elapsedTime: targetList.getElapsedTime(targetList.facetsToFetch[i].name), + sourceExplanation: targetList.facetsToFetch[i].description }); } return { @@ -1642,12 +1642,12 @@ function getDiseaseResult(args, tcrd) { for (var i in rows) { facets.push({ sql: queries[i].toString(), - elapsedTime: diseaseList.getElapsedTime(diseaseList.facetsToFetch[i].type), - facet: diseaseList.facetsToFetch[i].type, + elapsedTime: diseaseList.getElapsedTime(diseaseList.facetsToFetch[i].name), + facet: diseaseList.facetsToFetch[i].name, modifier: diseaseList.facetsToFetch[i].typeModifier, count: rows[i].length, values: rows[i], - sourceExplanation: diseaseList.facetsToFetch[i].sourceExplanation + sourceExplanation: diseaseList.facetsToFetch[i].description }) } @@ -1703,13 +1703,13 @@ function getLigandResult(args, tcrd) { facets.push({ dataType: ligandList.facetsToFetch[i].dataType == FacetDataType.numeric ? "Numeric" : "Category", binSize: ligandList.facetsToFetch[i].binSize, - facet: ligandList.facetsToFetch[i].type, + facet: ligandList.facetsToFetch[i].name, modifier: ligandList.facetsToFetch[i].typeModifier, count: rowData.length, values: rowData, sql: facetQueries[i].toString(), - elapsedTime: ligandList.getElapsedTime(ligandList.facetsToFetch[i].type), - sourceExplanation: ligandList.facetsToFetch[i].sourceExplanation + elapsedTime: ligandList.getElapsedTime(ligandList.facetsToFetch[i].name), + sourceExplanation: ligandList.facetsToFetch[i].description }); } return { From 67524acb0c460b262d2d7e6c942ba40c498f16ec Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Mon, 22 Mar 2021 17:26:07 -0400 Subject: [PATCH 02/19] polish up basic data download functionality --- .../diseaseQueries/comprehensive.test.js | 153 ++++++++++++++++++ src/__tests__/getListQuery.test.js | 108 ------------- .../{graphql.test.js.dont => graphql.test.js} | 0 .../ligandQueries/comprehensive.test.js | 150 +++++++++++++++++ .../allFacetQueries.test.js | 6 +- .../comprehensive.test.js} | 6 +- .../{ => targetQueries}/getFacetQuery.test.js | 88 ++-------- .../targetQueries/getListQuery.test.js | 135 ++++++++++++++++ src/models/DataModelList.ts | 70 ++++---- src/models/FacetFactory.ts | 15 +- src/models/FieldInfo.ts | 45 ++---- src/models/IBuildable.ts | 2 + src/models/config.ts | 60 ------- src/models/databaseConfig.ts | 71 ++++++-- src/models/databaseTable.ts | 10 +- src/models/disease/diseaseFacetFactory.ts | 33 ---- src/models/disease/diseaseList.ts | 59 +++++-- src/models/ligand/ligandFacetFactory.ts | 44 ----- src/models/ligand/ligandList.ts | 71 +++++--- src/models/queryDefinition.ts | 61 ++++--- src/models/sqlTable.ts | 5 +- src/models/target/targetFacetFactory.ts | 19 --- src/models/target/targetList.ts | 59 +++++-- src/resolvers.js | 129 +++++++++------ src/schema.graphql | 18 +-- 25 files changed, 850 insertions(+), 567 deletions(-) create mode 100644 src/__tests__/diseaseQueries/comprehensive.test.js delete mode 100644 src/__tests__/getListQuery.test.js rename src/__tests__/{graphql.test.js.dont => graphql.test.js} (100%) create mode 100644 src/__tests__/ligandQueries/comprehensive.test.js rename src/__tests__/{ => targetQueries}/allFacetQueries.test.js (94%) rename src/__tests__/{listAndFacetQueries.js => targetQueries/comprehensive.test.js} (98%) rename src/__tests__/{ => targetQueries}/getFacetQuery.test.js (66%) create mode 100644 src/__tests__/targetQueries/getListQuery.test.js delete mode 100644 src/models/config.ts delete mode 100644 src/models/disease/diseaseFacetFactory.ts delete mode 100644 src/models/ligand/ligandFacetFactory.ts delete mode 100644 src/models/target/targetFacetFactory.ts diff --git a/src/__tests__/diseaseQueries/comprehensive.test.js b/src/__tests__/diseaseQueries/comprehensive.test.js new file mode 100644 index 0000000..e0b6d3a --- /dev/null +++ b/src/__tests__/diseaseQueries/comprehensive.test.js @@ -0,0 +1,153 @@ +const {DiseaseList} = require("../../models/disease/diseaseList"); +const {tcrdConfig} = require('../../__utils/loadTCRDforTesting'); +const TCRD = require('../../TCRD'); + +let tcrd = new TCRD(tcrdConfig); + +beforeAll(() => { + return tcrd.tableInfo.loadPromise.then((res) => { + return; + }); +}); + +describe('all the queries should be consistent with each other', function () { + test('All diseases query', () => { + const fullList = new DiseaseList(tcrd, {top:20000}); + const filteredList = new DiseaseList(tcrd, {top:20000, filter: {facets: [{facet: "Data Source", values: ["CTD"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullTypeFacet = fullList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const fullTypeFacetQuery = fullTypeFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredTypeFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const filteredTypeFacetQuery = filteredTypeFacet.getFacetQuery(); + const filteredTypeConstraintQuery = filteredList.filteringFacets[0].getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullTypeFacetQuery, + filteredCountQuery, filteredListQuery, filteredTypeFacetQuery, + filteredTypeConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullTypeCount = res[2].reduce((a, c) => a + c.value, 0); + expect(fullTypeCount).toBeGreaterThanOrEqual(fullListLength); + + const filteredCount = res[3][0].count; + const filteredListLength = res[4].length; + expect(filteredCount).toBe(filteredListLength); + const filteredTypeCount = res[5].reduce((a, c) => a + c.value, 0); + expect(filteredTypeCount).toBeGreaterThanOrEqual(filteredListLength); + + const facetConstraintList = res[6].length; + + expect(facetConstraintList).toBe(filteredListLength); + expect(fullCount).toBeGreaterThan(filteredCount); + expect(fullTypeCount).toBeGreaterThan(filteredTypeCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + + + test('term search query', () => { + const fullList = new DiseaseList(tcrd, {top:20000, filter: {term:"cancer"}}); + const filteredList = new DiseaseList(tcrd, {top:20000, filter: {term:"cancer", facets: [{facet: "Data Source", values: ["CTD"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullTypeFacet = fullList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const fullTypeFacetQuery = fullTypeFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredTypeFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Target Development Level'); + const filteredTypeFacetQuery = filteredTypeFacet.getFacetQuery(); + const filteredTypeConstraintQuery = filteredList.filteringFacets[0].getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullTypeFacetQuery, + filteredCountQuery, filteredListQuery, filteredTypeFacetQuery, + filteredTypeConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullTypeCount = res[2].reduce((a, c) => a + c.value, 0); + expect(fullTypeCount).toBeGreaterThanOrEqual(fullListLength); // only true for TDL because of multiple targets associated with these diseases + // ideally they'd be equal and add up exactly like in the target / ligand test facets + const filteredCount = res[3][0].count; + const filteredListLength = res[4].length; + expect(filteredCount).toBe(filteredListLength); + const filteredTypeCount = res[5].reduce((a, c) => a + c.value, 0); + expect(filteredTypeCount).toBeGreaterThanOrEqual(filteredListLength); + + const facetConstraintList = res[6].length; + + // expect(facetConstraintList).toBe(filteredListLength); + expect(fullCount).toBeGreaterThan(filteredCount); + expect(fullTypeCount).toBeGreaterThan(filteredTypeCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + + test('associated target query', () => { + const fullList = new DiseaseList(tcrd, {top:20000, filter: {associatedTarget:"DRD2"}}); + const filteredList = new DiseaseList(tcrd, {top:20000, filter: {associatedTarget:"DRD2", facets: [{facet: "Data Source", values: ["CTD"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullTypeFacet = fullList.facetsToFetch.find(facet => facet.name === 'Drug'); + const fullTypeFacetQuery = fullTypeFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredTypeFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Drug'); + const filteredTypeFacetQuery = filteredTypeFacet.getFacetQuery(); + const filteredTypeConstraintQuery = filteredList.filteringFacets[0].getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullTypeFacetQuery, + filteredCountQuery, filteredListQuery, filteredTypeFacetQuery, + filteredTypeConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullTypeCount = res[2].reduce((a, c) => a + c.value, 0); + // expect(fullTypeCount).toBeGreaterThanOrEqual(fullListLength); // not true for drugs because they are sparse + + const filteredCount = res[3][0].count; + const filteredListLength = res[4].length; + expect(filteredCount).toBe(filteredListLength); + const filteredTypeCount = res[5].reduce((a, c) => a + c.value, 0); + // expect(filteredTypeCount).toBeGreaterThanOrEqual(filteredListLength); + + const facetConstraintList = res[6].length; + + // expect(facetConstraintList).toBe(filteredListLength); + expect(fullCount).toBeGreaterThan(filteredCount); + expect(fullTypeCount).toBeGreaterThan(filteredTypeCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + +}); diff --git a/src/__tests__/getListQuery.test.js b/src/__tests__/getListQuery.test.js deleted file mode 100644 index f43a61f..0000000 --- a/src/__tests__/getListQuery.test.js +++ /dev/null @@ -1,108 +0,0 @@ -const {TargetList} = require("../models/target/targetList"); -const {tcrdConfig} = require('../__utils/loadTCRDforTesting'); -const TCRD = require('../TCRD'); - -let tcrd = new TCRD(tcrdConfig); - -beforeAll(() => { - return tcrd.tableInfo.loadPromise.then((res) => { - return; - }); -}); - -describe('list queries should work', function () { - test('protein - target linking', () => { - const listObj = new TargetList(tcrd, { - filter: {facets: [{facet: "Family", values: ["Kinase", "Transcription Factor"]}]}, - top: 10, - fields: ["Target Development Level", "Family"] - }); - // console.log(listObj.getListQuery().toString()); - return listObj.getListQuery().then(res => { - expect(res.length).toBe(10); - expect(res[0]["Target Development Level"]).toBeTruthy(); - expect(res[0]["Family"]).toBeTruthy(); - }); - }); - test('download query should work with an extra join constraint', () => { - const listObj = new TargetList(tcrd, { - filter: {facets: [{facet: "Family", values: ["Kinase", "Transcription Factor"]}]}, - top: 10, - fields: ["UniProt Keyword"] - }); - // console.log(listObj.getListQuery().toString()); - return listObj.getListQuery().then(res => { - expect(res.length).toBe(10); - expect(res[0]["UniProt Keyword"]).toBeTruthy(); - }); - }); - test('download query should work with duplicated tables', () => { - const listObj = new TargetList(tcrd, { - filter: {facets: [{facet: "Data Source", values: ["RCSB Protein Data Bank"]}]}, - top: 10, - fields: ["UniProt Keyword", "PDB ID"] - }); - // console.log(listObj.getListQuery().toString()); - return listObj.getListQuery().then(res => { - expect(res.length).toBe(10); - expect(res[0]["UniProt Keyword"]).toBeTruthy(); - expect(res[0]["PDB ID"]).toBeTruthy(); - }); - }); - test('group method = count groups and counts', () => { - const listObj = new TargetList(tcrd, { - filter: {facets: [{facet: "Family", values: ["Kinase", "Transcription Factor"]}]}, - top: 10, - fields: ["Family", "UniProt", "PDB Count"] - }); - // console.log(listObj.getListQuery().toString()); - return listObj.getListQuery().then(res => { - expect(res.length).toBe(10); - res.forEach(val => { - expect(val["UniProt"]).toBeTruthy(); - expect(val["PDB Count"]).toBeGreaterThanOrEqual(0); - }); - }); - }); - - test('list with an associated target', () => { - const listObj = new TargetList(tcrd, { - filter: { - associatedTarget: 'ACE2', - facets: [{facet: "PPI Data Source", values: ["BioPlex"]}] - }, - top: 10, - fields: [ - "UniProt", - "Symbol", - "PPI Data Source", - "StringDB Interaction Score", - "BioPlex Interaction Probability", - "BioPlex p_ni", - "BioPlex p_wrong"] - }); - // console.log(listObj.getListQuery().toString()); - return listObj.getListQuery().then(res => { - expect(res.length).toBeGreaterThan(0); - expect(res[0]["UniProt"]).toBeTruthy(); - expect(res[0]["Symbol"]).toBeTruthy(); - expect(res[0]["PPI Data Source"]).toBeTruthy(); - expect(res[0]["StringDB Interaction Score"]).toBeGreaterThan(0); - expect(res[0]["BioPlex Interaction Probability"]).toBeGreaterThan(0); - expect(res[0]["BioPlex p_ni"]).toBeGreaterThan(0); - expect(res[0]["BioPlex p_wrong"]).toBeGreaterThan(0); - }); - // expecting data like this, a new TCRD might return something with some nulls - // RowDataPacket { - // id: 16144, - // UniProt: 'Q13685', - // Symbol: 'AAMP', - // 'PPI Data Source': 'BioPlex,STRINGDB', - // 'StringDB Interaction Score': 0.519, - // 'BioPlex Interaction Probability': 0.821892143, - // 'BioPlex p_ni': 1.5e-8, - // 'BioPlex p_wrong': 0.178107842 - // } - }); - -}); diff --git a/src/__tests__/graphql.test.js.dont b/src/__tests__/graphql.test.js similarity index 100% rename from src/__tests__/graphql.test.js.dont rename to src/__tests__/graphql.test.js diff --git a/src/__tests__/ligandQueries/comprehensive.test.js b/src/__tests__/ligandQueries/comprehensive.test.js new file mode 100644 index 0000000..d6dc2f9 --- /dev/null +++ b/src/__tests__/ligandQueries/comprehensive.test.js @@ -0,0 +1,150 @@ +const {LigandList} = require("../../models/ligand/ligandList"); +const {tcrdConfig} = require('../../__utils/loadTCRDforTesting'); +const TCRD = require('../../TCRD'); + +let tcrd = new TCRD(tcrdConfig); + +beforeAll(() => { + return tcrd.tableInfo.loadPromise.then((res) => { + return; + }); +}); + +describe('all the queries should be consistent with each other', function () { + test('All ligands query', () => { + const fullList = new LigandList(tcrd, {top:2669440}); + const filteredList = new LigandList(tcrd, {top:2669440, filter: {facets: [{facet: "Activity", values: ["IC50"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullTypeFacet = fullList.facetsToFetch.find(facet => facet.name === 'Type'); + const fullTypeFacetQuery = fullTypeFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredTypeFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Type'); + const filteredTypeFacetQuery = filteredTypeFacet.getFacetQuery(); + const filteredTypeConstraintQuery = filteredList.filteringFacets[0].getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullTypeFacetQuery, + filteredCountQuery, filteredListQuery, filteredTypeFacetQuery, + filteredTypeConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullTypeCount = res[2].reduce((a, c) => a + c.value, 0); + expect(fullTypeCount).toBe(fullListLength); + + const filteredCount = res[3][0].count; + const filteredListLength = res[4].length; + expect(filteredCount).toBe(filteredListLength); + const filteredTypeCount = res[5].reduce((a, c) => a + c.value, 0); + expect(filteredTypeCount).toBe(filteredListLength); + + const facetConstraintList = res[6].length; + + expect(facetConstraintList).toBe(filteredListLength); + expect(fullCount).toBeGreaterThan(filteredCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + + test('Search term query', () => { + const fullList = new LigandList(tcrd, {top:2669440, filter: {term:'iso'}}); + const filteredList = new LigandList(tcrd, {top:2669440, filter: {term:'iso', facets: [{facet: "Activity", values: ["IC50"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullTypeFacet = fullList.facetsToFetch.find(facet => facet.name === 'Type'); + const fullTypeFacetQuery = fullTypeFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredTypeFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Type'); + const filteredTypeFacetQuery = filteredTypeFacet.getFacetQuery(); + const filteredTypeConstraintQuery = filteredList.filteringFacets[0].getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullTypeFacetQuery, + filteredCountQuery, filteredListQuery, filteredTypeFacetQuery, + filteredTypeConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullTypeCount = res[2].reduce((a, c) => a + c.value, 0); + expect(fullTypeCount).toBe(fullListLength); + + const filteredCount = res[3][0].count; + const filteredListLength = res[4].length; + expect(filteredCount).toBe(filteredListLength); + const filteredTypeCount = res[5].reduce((a, c) => a + c.value, 0); + expect(filteredTypeCount).toBe(filteredListLength); + + const facetConstraintList = res[6].length; + + // expect(facetConstraintList).toBe(filteredListLength); // not true anymore because the facets are only constraining their one thing + + expect(fullCount).toBeGreaterThan(filteredCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); + + test('associated target queries', () => { + const fullList = new LigandList(tcrd, {top:2669440, filter: {associatedTarget:'ACE2'}}); + const filteredList = new LigandList(tcrd, {top:2669440, filter: {associatedTarget:'ACE2', facets: [{facet: "Activity", values: ["IC50"]}]}}); + + const fullCountQuery = fullList.getCountQuery(); + const fullListQuery = fullList.getListQuery(); + const fullTypeFacet = fullList.facetsToFetch.find(facet => facet.name === 'Type'); + const fullTypeFacetQuery = fullTypeFacet.getFacetQuery(); + + const filteredCountQuery = filteredList.getCountQuery(); + const filteredListQuery = filteredList.getListQuery(); + const filteredTypeFacet = filteredList.facetsToFetch.find(facet => facet.name === 'Type'); + const filteredTypeFacetQuery = filteredTypeFacet.getFacetQuery(); + const filteredTypeConstraintQuery = filteredList.filteringFacets[0].getFacetConstraintQuery(); + + return Promise.all( + [fullCountQuery, fullListQuery, fullTypeFacetQuery, + filteredCountQuery, filteredListQuery, filteredTypeFacetQuery, + filteredTypeConstraintQuery]) + .then(res => { + const fullCount = res[0][0].count; + const fullListLength = res[1].length; + expect(fullCount).toBe(fullListLength); + const fullTypeCount = res[2].reduce((a, c) => a + c.value, 0); + expect(fullTypeCount).toBe(fullListLength); + + const filteredCount = res[3][0].count; + const filteredListLength = res[4].length; + expect(filteredCount).toBe(filteredListLength); + const filteredTypeCount = res[5].reduce((a, c) => a + c.value, 0); + expect(filteredTypeCount).toBe(filteredListLength); + + const facetConstraintList = res[6].length; + + // expect(facetConstraintList).toBe(filteredListLength); // not true anymore because the facets are only constraining their one thing + + expect(fullCount).toBeGreaterThan(filteredCount); + + expect(fullCount).toBeGreaterThan(0); + expect(filteredCount).toBeGreaterThan(0); + + console.log(fullCount); + console.log(filteredCount); + }); + }); +}); diff --git a/src/__tests__/allFacetQueries.test.js b/src/__tests__/targetQueries/allFacetQueries.test.js similarity index 94% rename from src/__tests__/allFacetQueries.test.js rename to src/__tests__/targetQueries/allFacetQueries.test.js index 82543f3..1286cef 100644 --- a/src/__tests__/allFacetQueries.test.js +++ b/src/__tests__/targetQueries/allFacetQueries.test.js @@ -1,6 +1,6 @@ -const {TargetList} = require("../models/target/targetList"); -const {tcrdConfig} = require('../__utils/loadTCRDforTesting'); -const TCRD = require('../TCRD'); +const {TargetList} = require("../../models/target/targetList"); +const {tcrdConfig} = require('../../__utils/loadTCRDforTesting'); +const TCRD = require('../../TCRD'); let tcrd = new TCRD(tcrdConfig); diff --git a/src/__tests__/listAndFacetQueries.js b/src/__tests__/targetQueries/comprehensive.test.js similarity index 98% rename from src/__tests__/listAndFacetQueries.js rename to src/__tests__/targetQueries/comprehensive.test.js index fc283bc..a8b6e30 100644 --- a/src/__tests__/listAndFacetQueries.js +++ b/src/__tests__/targetQueries/comprehensive.test.js @@ -1,6 +1,6 @@ -const {TargetList} = require("../models/target/targetList"); -const {tcrdConfig} = require('../__utils/loadTCRDforTesting'); -const TCRD = require('../TCRD'); +const {TargetList} = require("../../models/target/targetList"); +const {tcrdConfig} = require('../../__utils/loadTCRDforTesting'); +const TCRD = require('../../TCRD'); let tcrd = new TCRD(tcrdConfig); diff --git a/src/__tests__/getFacetQuery.test.js b/src/__tests__/targetQueries/getFacetQuery.test.js similarity index 66% rename from src/__tests__/getFacetQuery.test.js rename to src/__tests__/targetQueries/getFacetQuery.test.js index 302cc9a..5778b07 100644 --- a/src/__tests__/getFacetQuery.test.js +++ b/src/__tests__/targetQueries/getFacetQuery.test.js @@ -1,6 +1,6 @@ -const {TargetList} = require("../models/target/targetList"); -const {tcrdConfig} = require('../__utils/loadTCRDforTesting'); -const TCRD = require('../TCRD'); +const {TargetList} = require("../../models/target/targetList"); +const {tcrdConfig} = require('../../__utils/loadTCRDforTesting'); +const TCRD = require('../../TCRD'); let tcrd = new TCRD(tcrdConfig); @@ -11,47 +11,6 @@ beforeAll(() => { }); describe('list queries should work', function () { - test('TDL facet looks right', () => { - const listObj = new TargetList(tcrd, {}); - - const facet = listObj.facetsToFetch.find(facet => facet.name === "Target Development Level"); - const query = facet.getFacetQuery(); - return query.then(res => { - expect(res.length).toBe(4); - res.forEach(row => { - expect(row.name).toBeTruthy(); - expect(row.value).toBeGreaterThanOrEqual(0); - }); - }); - }); - - - test('Facet with custom select looks right', () => { - const listObj = new TargetList(tcrd, {facets: ["Family"]}); - const query = listObj.facetsToFetch.find(facet => facet.name === "Family").getFacetQuery(); - return query.then(res => { - const TFrow = res.find(row => row.name == 'Transcription Factor'); // translated from TF - expect(TFrow.name).toBe('Transcription Factor'); - res.slice(0, 5).forEach(row => { - expect(row.name).toBeTruthy(); - expect(row.value).toBeGreaterThanOrEqual(0); - }); - }); - }); - - test('facet constraints are constraining something', () => { - const listObj = new TargetList(tcrd, {filter: {facets: [{facet: "Family", values: ["Kinase"]}]}}); - const tdlFacet = listObj.facetsToFetch.find(facet => facet.name === "Target Development Level").getFacetQuery(); - const famFacet = listObj.facetsToFetch.find(facet => facet.name === "Family").getFacetQuery(); - return Promise.all([tdlFacet, famFacet]).then(res => { - const tdlData = res[0]; - const famData = res[1]; - const tdlCount = tdlData.reduce((a, c) => a + c.value, 0); - const famCount = famData.reduce((a, c) => a + c.value, 0); - - expect(tdlCount).toBeLessThan(famCount); // b/c TDL is filtered by Fam facet, and Fam facet doesn't filter itself - }); - }); test('numeric facets work too!', () => { const listObj = new TargetList(tcrd, {facets: ["Log Novelty"]}); @@ -89,31 +48,6 @@ describe('list queries should work', function () { }); }); - test('Facet constraint queries are people too!', () => { - const listObj = new TargetList(tcrd, { - filter: { - facets: [{ - facet: "Family", - values: ["Kinase", "Transcription Factor"] - }] - } - }); - const famFacet = listObj.facetsToFetch.find(facet => facet.name === "Family"); - return Promise.all([famFacet.getFacetQuery(), famFacet.getFacetConstraintQuery()]).then(res => { - const famCounts = res[0]; - const proteinList = res[1]; - - const kinaseCount = famCounts.find(row => row.name == 'Kinase').value; - const tfCount = famCounts.find(row => row.name == 'Transcription Factor').value; - - expect(proteinList.length).toBe(kinaseCount + tfCount); - - proteinList.slice(0, 5).forEach(row => { - expect(row.id).toBeGreaterThan(0); - }); - }); - }); - test('Facet constraint sometimes have where clauses', () => { const listObj = new TargetList(tcrd, {filter: {facets: [{facet: "UniProt Keyword", values: ["Lyase"]}]}}); const upFacet = listObj.facetsToFetch.find(facet => facet.name === "UniProt Keyword"); @@ -179,8 +113,8 @@ describe('list queries should work', function () { }); const facet = listObj.facetsToFetch.find(facet => facet.name === "PPI Data Source"); - console.log(facet.getFacetQuery().toString()); - console.log(facet.getFacetConstraintQuery().toString()); + // console.log(facet.getFacetQuery().toString()); + // console.log(facet.getFacetConstraintQuery().toString()); return Promise.all([facet.getFacetQuery(), facet.getFacetConstraintQuery()]).then(res => { const countResults = res[0]; @@ -206,12 +140,10 @@ describe('list queries should work', function () { const ppiFacet = ppiList.facetsToFetch.find(facet => facet.name === "Target Development Level"); const fullFacet = fullList.facetsToFetch.find(facet => facet.name === "Target Development Level"); - console.log(ppiFacet.getFacetQuery().toString()); - console.log(fullFacet.getFacetQuery().toString()); + // console.log(ppiFacet.getFacetQuery().toString()); + // console.log(fullFacet.getFacetQuery().toString()); return Promise.all([ppiFacet.getFacetQuery(), fullFacet.getFacetQuery()]).then(res => { - console.log(res); - const ppiResults = res[0]; const fullResults = res[1]; @@ -228,11 +160,10 @@ describe('list queries should work', function () { const fullList = new TargetList(tcrd, {filter: {facets: [{facet: "Log Novelty", values: ["[ -5.5, -3.5 )"]}]}}); const fullFacet = fullList.facetsToFetch.find(facet => facet.name === "Target Development Level"); - console.log(ppiFacet.getFacetQuery().toString()); - console.log(fullFacet.getFacetQuery().toString()); + // console.log(ppiFacet.getFacetQuery().toString()); + // console.log(fullFacet.getFacetQuery().toString()); return Promise.all([ppiFacet.getFacetQuery(), fullFacet.getFacetQuery()]).then(res => { - console.log(res); const ppiResults = res[0]; const fullResults = res[1]; @@ -240,7 +171,6 @@ describe('list queries should work', function () { const fullCount = fullResults.reduce((a, c) => a + c.value, 0); expect(fullCount).toBeGreaterThan(ppiCount); - }); }); diff --git a/src/__tests__/targetQueries/getListQuery.test.js b/src/__tests__/targetQueries/getListQuery.test.js new file mode 100644 index 0000000..1a84b60 --- /dev/null +++ b/src/__tests__/targetQueries/getListQuery.test.js @@ -0,0 +1,135 @@ +const {TargetList} = require("../../models/target/targetList"); +const {tcrdConfig} = require('../../__utils/loadTCRDforTesting'); +const TCRD = require('../../TCRD'); + +let tcrd = new TCRD(tcrdConfig); + +beforeAll(() => { + return tcrd.tableInfo.loadPromise.then((res) => { + return; + }); +}); + +describe('list queries should work', function () { + test('download query should work with duplicated tables', () => { + const listObj = new TargetList(tcrd, { + filter: {facets: [{facet: "Data Source", values: ["RCSB Protein Data Bank"]}]}, + top: 10, + fields: ["UniProt Keyword", "PDB IDs"] + }); + // console.log(listObj.getListQuery().toString()); + return listObj.getListQuery().then(res => { + expect(res.length).toBe(10); + expect(res[0]["UniProt Keyword"]).toBeTruthy(); + expect(res[0]["PDB IDs"]).toBeTruthy(); + }); + }); + test('group method = count groups and counts', () => { + const listObj = new TargetList(tcrd, { + filter: {facets: [{facet: "Family", values: ["Kinase", "Transcription Factor"]}, {facet: "Data Source", values: ["RCSB Protein Data Bank"]}]}, + top: 10, + fields: ["Family", "UniProt", "PDB IDs"] + }); + console.log(listObj.getListQuery().toString()); + return listObj.getListQuery().then(res => { + expect(res.length).toBe(10); + res.forEach(val => { + expect(val["UniProt"]).toBeTruthy(); + expect(val["PDB IDs"].length).toBeGreaterThan(0); + }); + }); + }); + + test('list with an associated target', () => { + const listObj = new TargetList(tcrd, { + filter: { + associatedTarget: 'ACE2', + facets: [{facet: "PPI Data Source", values: ["BioPlex"]}] + }, + top: 10, + fields: [ + "UniProt", + "Symbol", + "PPI Data Source", + "StringDB Interaction Score", + "BioPlex Interaction Probability", + "BioPlex p_ni", + "BioPlex p_wrong"] + }); + console.log(listObj.getListQuery().toString()); + return listObj.getListQuery().then(res => { + expect(res.length).toBeGreaterThan(0); + expect(res[0]["UniProt"]).toBeTruthy(); + expect(res[0]["Symbol"]).toBeTruthy(); + expect(res[0]["PPI Data Source"]).toBeTruthy(); + expect(res[0]["StringDB Interaction Score"]).toBeGreaterThan(0); + expect(res[0]["BioPlex Interaction Probability"]).toBeGreaterThan(0); + expect(res[0]["BioPlex p_ni"]).toBeGreaterThan(0); + expect(res[0]["BioPlex p_wrong"]).toBeGreaterThan(0); + }); + }); + + + test('Sorting by a column that isnt normally there', () => { + const descendingList = new TargetList(tcrd, { + top: 10, + filter: { + associatedTarget: "ACE2", + order: "!Antibody Count" + } + }); + const ascendingList = new TargetList(tcrd, { + top: 10, + filter: { + associatedTarget: 'ACE2', + order: "^Antibody Count" + } + }); + const descQuery = descendingList.getListQuery(); + const ascQuery = ascendingList.getListQuery(); + return Promise.all([descQuery, ascQuery]).then(res => { + const descRes = res[0].map(r => r['Antibody Count']); + const ascRes = res[1].map(r => r['Antibody Count']); + expect(descRes.length).toBe(10); + expect(ascRes.length).toBe(10); + let lastDescVal = descRes[0]; + let lastAscVal = ascRes[0]; + for (let i = 1; i < 10; i++) { + const nextDescVal = descRes[i]; + const nextAscVal = ascRes[i]; + expect(nextDescVal).toBeLessThanOrEqual(lastDescVal); + expect(nextAscVal).toBeGreaterThanOrEqual(lastAscVal); + lastDescVal = nextDescVal; + lastAscVal = nextAscVal; + } + }) + }); + + test('target batches are a thing', () => { + const proteinList = ["Q13619", "Q13616", "P06493", "Q92879", "GPR85", "ACE2"]; + const batchList = new TargetList(tcrd, { + batch: proteinList + }); + const listQuery = batchList.getListQuery(); + const tdlFacet = batchList.facetsToFetch.find(f => f.name == 'Target Development Level').getFacetQuery(); + const famFacet = batchList.facetsToFetch.find(f => f.name == 'Family').getFacetQuery(); + const countQuery = batchList.getCountQuery(); + + return Promise.all([countQuery, listQuery, tdlFacet, famFacet]).then(res => { + const resultCount = res[0][0].count; + const listLength = res[1].length; + const tdlCount = res[2].reduce((a, c) => a + c.value, 0); + const famCount = res[3].reduce((a, c) => a + c.value, 0); + expect(resultCount).toBe(proteinList.length); + expect(listLength).toBe(proteinList.length); + expect(tdlCount).toBe(proteinList.length); + expect(famCount).toBe(proteinList.length); + + res[1].forEach(row => { + const foundSymbol = proteinList.includes(row.sym); + const foundUniProt = proteinList.includes(row.uniprot); + expect(foundSymbol || foundUniProt).toBe(true); + }); + }) + }); +}); diff --git a/src/models/DataModelList.ts b/src/models/DataModelList.ts index 683e285..cc52ef1 100644 --- a/src/models/DataModelList.ts +++ b/src/models/DataModelList.ts @@ -1,21 +1,20 @@ import now from "performance-now"; import {FieldInfo} from "./FieldInfo"; import {FacetFactory} from "./FacetFactory"; -import {DatabaseConfig} from "./databaseConfig"; +import {DatabaseConfig, ModelInfo} from "./databaseConfig"; // @ts-ignore import * as CONSTANTS from "../constants"; import {QueryDefinition} from "./queryDefinition"; import {IBuildable} from "./IBuildable"; +import {SqlTable} from "./sqlTable"; export abstract class DataModelList implements IBuildable { - abstract addModelSpecificFiltering(query: any, list?: boolean): void; - abstract getDefaultFields(): FieldInfo[]; - abstract defaultSortParameters(): { column: string, order: string }[]; + abstract addModelSpecificFiltering(query: any, list: boolean, tables: string[]): void; + abstract getAvailableListFields(): FieldInfo[]; + abstract defaultSortParameters(): { column: string, order: string } []; - - getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string { - return ''; - } + abstract getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string; + abstract tableNeedsInnerJoin(table: SqlTable): boolean; batch: string[] = []; facetFactory: FacetFactory; @@ -25,6 +24,7 @@ export abstract class DataModelList implements IBuildable { keyColumn: string; filteringFacets: FieldInfo[] = []; facetsToFetch: FieldInfo[] = []; + dataFields: FieldInfo[] = []; associatedTarget: string = ""; associatedDisease: string = ""; @@ -32,7 +32,7 @@ export abstract class DataModelList implements IBuildable { ppiConfidence: number = CONSTANTS.DEFAULT_PPI_CONFIDENCE; skip: number = 0; top: number = 10; - modelInfo: { name: string, table: string, column: string }; + modelInfo: ModelInfo = {name: '', table: '', column: ''}; tcrd: any; database: any; @@ -61,15 +61,18 @@ export abstract class DataModelList implements IBuildable { return fieldInfo.map((facet: FieldInfo) => facet.name) || []; }; - constructor(tcrd: any, rootTable: string, keyColumn: string, facetFactory: FacetFactory, json: any, extra?: any) { + constructor(tcrd: any, modelName: string, json: any, extra?: any) { this.tcrd = tcrd; this.database = tcrd.db; this.databaseConfig = tcrd.tableInfo; - this.rootTable = rootTable; - this.keyColumn = keyColumn || this.databaseConfig.getPrimaryKey(this.rootTable); - this.facetFactory = facetFactory; - - this.modelInfo = this.databaseConfig.modelList.get(this.rootTable) || { name: '', table: '', column: '' }; + // @ts-ignore + this.modelInfo = this.databaseConfig.modelList.get(modelName); + if(!this.modelInfo){ + throw new Error('Unknown model: ' + modelName); + } + this.rootTable = this.modelInfo.table; + this.keyColumn = this.modelInfo.column || this.databaseConfig.getPrimaryKey(this.rootTable); + this.facetFactory = new FacetFactory(); if (json) { if (json.batch) { @@ -114,10 +117,6 @@ export abstract class DataModelList implements IBuildable { } if (json.filter.order) { this.sortField = json.filter.order.substring(1); - // if (this.sortField.indexOf('.') > 0) { - // this.sortTable = this.sortField.split('.')[0]; - // this.sortField = this.sortField.split('.')[1]; - // } let ch = json.filter.order.charAt(0); this.direction = (ch == '^') ? 'asc' : 'desc'; } @@ -152,7 +151,7 @@ export abstract class DataModelList implements IBuildable { [{table: this.rootTable, column: this.keyColumn, group_method: 'count', alias: 'count'} as FieldInfo]); const query = queryDefinition.generateBaseQuery(false); this.addFacetConstraints(query, this.filteringFacets); - this.addModelSpecificFiltering(query); + this.addModelSpecificFiltering(query, false, []); this.captureQueryPerformance(query, "list count"); return query; }; @@ -160,13 +159,14 @@ export abstract class DataModelList implements IBuildable { getListQuery() { let dataFields: FieldInfo[]; if (this.fields && this.fields.length > 0) { - dataFields = this.GetDataFields(); + dataFields = this.GetDataFields('list'); } else { - dataFields = this.getDefaultFields(); + dataFields = this.getAvailableListFields(); } if(!dataFields.map(f => f.name).includes(this.sortField)) { - this.pushOneDataField(this.sortField, dataFields); + this.pushOneDataField(this.sortField, 'list', dataFields); } + this.dataFields = dataFields; const sortField = dataFields.find(f => f.name === this.sortField); const queryDefinition = QueryDefinition.GenerateQueryDefinition(this, dataFields); @@ -174,7 +174,7 @@ export abstract class DataModelList implements IBuildable { const query = queryDefinition.generateBaseQuery(false); this.addFacetConstraints(query, this.filteringFacets); - this.addModelSpecificFiltering(query, true); + this.addModelSpecificFiltering(query, true, this.dataFields.map(f => f.table)); if (queryDefinition.hasGroupedColumns()) { query.groupBy(this.keyString()); @@ -186,8 +186,8 @@ export abstract class DataModelList implements IBuildable { if (this.top) { query.limit(this.top); } + // console.log(query.toString()); return query; - } addSort(query: any, queryDefinition: QueryDefinition, sortFieldInfo: FieldInfo | undefined) { @@ -241,20 +241,19 @@ export abstract class DataModelList implements IBuildable { return true; } - GetDataFields(): FieldInfo[] { + GetDataFields(context: string): FieldInfo[] { const dataFields: FieldInfo[] = []; - dataFields.push(new FieldInfo({table: this.modelInfo.table, column: this.modelInfo.column, alias: 'id'} as FieldInfo)); this.fields.forEach(field => { - this.pushOneDataField(field, dataFields); + this.pushOneDataField(field, context, dataFields); }); return dataFields; } - private pushOneDataField(field: string, dataFields: FieldInfo[]) { - const fieldInfo = this.databaseConfig.getOneField(this.modelInfo.name, '', this.getAssociatedModel(), '', field); + private pushOneDataField(field: string, context: string, dataFields: FieldInfo[]) { + const fieldInfo = this.databaseConfig.getOneField(this.modelInfo.name, context, this.getAssociatedModel(), '', field); if (fieldInfo) { fieldInfo.alias = field; dataFields.push(fieldInfo); @@ -281,17 +280,6 @@ export abstract class DataModelList implements IBuildable { } return -1; } - - static listToObject(list: string[], lastTable: string) { - let obj: any = {}; - for (let i = 0; i < list.length; i++) { - if (lastTable != list[i]) { - obj[list[i]] = list[i]; - } - } - obj[lastTable] = lastTable; // because apparently it matters the order you add fields to an object, somehow knex adds tables in this order - return obj; - } } class QueryPerformanceData { diff --git a/src/models/FacetFactory.ts b/src/models/FacetFactory.ts index 96cfe4b..316f3ef 100644 --- a/src/models/FacetFactory.ts +++ b/src/models/FacetFactory.ts @@ -1,8 +1,19 @@ import {DataModelList} from "./DataModelList"; import {FieldInfo} from "./FieldInfo"; -export abstract class FacetFactory { - abstract GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FieldInfo; +export class FacetFactory { + GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], nullQuery: boolean = false): FieldInfo { + const fieldInfo = parent.databaseConfig.getOneField(parent.modelInfo.name, 'facet', parent.getAssociatedModel(), '', typeName); + if(!fieldInfo){ + return this.unknownFacet(); + } + fieldInfo.allowedValues = allowedValues; + fieldInfo.parent = parent; + if (typeName === "PPI Data Source") { // :( + fieldInfo.valuesDelimited = true; + } + return fieldInfo; + } getFacetsFromList(parent: DataModelList, list: string[], extra?: any): FieldInfo[] { let facetList: FieldInfo[] = []; diff --git a/src/models/FieldInfo.ts b/src/models/FieldInfo.ts index 9b995ad..b2b710a 100644 --- a/src/models/FieldInfo.ts +++ b/src/models/FieldInfo.ts @@ -43,6 +43,14 @@ export class FieldInfo { this.table = obj?.table || ''; this.column = obj?.column || ''; + if(obj?.column){ + if(obj.column.includes(' ')){ + this.column = '`' + obj.column + '`'; + } + else{ + this.column = obj.column; + } + } this.alias = obj?.alias || this.column; this.where_clause = obj?.where_clause || ''; this.group_method = obj?.group_method || ''; @@ -78,34 +86,6 @@ export class FieldInfo { return new FieldInfo(this); } - // static parseFromConfigTable(parent: DataModelList, typeName: string, allowedValues: string[], nullQuery: boolean, facet_config: any): FieldInfo { - // const hasNullQueryFields = () => { - // return !!facet_config?.null_count_column; - // }; - // - // const returnObj: FieldInfo = {} as FieldInfo; - // returnObj.name = typeName; - // returnObj.parent = parent; - // returnObj.allowedValues = allowedValues; - // returnObj.select = facet_config?.select; - // returnObj.dataType = facet_config?.dataType || 'category'; - // returnObj.binSize = facet_config?.binSize; - // returnObj.log = facet_config?.log; - // returnObj.sourceExplanation = facet_config?.sourceExplanation; - // returnObj.groupMethod = facet_config?.group_method; - // if (nullQuery && hasNullQueryFields()) { - // returnObj.dataTable = facet_config?.null_table; - // returnObj.dataColumn = facet_config?.null_column; - // returnObj.whereClause = facet_config?.null_where_clause; - // returnObj.countColumn = facet_config?.null_count_column; - // } else { - // returnObj.dataTable = facet_config?.dataTable; - // returnObj.dataColumn = facet_config?.dataColumn; - // returnObj.whereClause = facet_config?.whereClause; - // } - // return new FieldInfo(returnObj); - // } - numericBounds() { const scrubText = function (text: string): number | null { if (!text) return null; @@ -135,7 +115,8 @@ export class FieldInfo { associatedDisease: this.parent.associatedDisease, rootTable: this.table, ppiConfidence: this.parent.ppiConfidence, - getSpecialModelWhereClause: this.parent.getSpecialModelWhereClause + getSpecialModelWhereClause: this.parent.getSpecialModelWhereClause.bind(this.parent), + tableNeedsInnerJoin: this.parent.tableNeedsInnerJoin.bind(this.parent) }, [ new FieldInfo({ table: this.parent.rootTable, @@ -182,6 +163,7 @@ export class FieldInfo { query = this.getStandardFacetQuery(); } this.parent.captureQueryPerformance(query, this.name); + // console.log(query.toString()); return query; } @@ -207,7 +189,7 @@ export class FieldInfo { let query = queryDefinition.generateBaseQuery(true); this.parent.addFacetConstraints(query, this.parent.filteringFacets, this.name); - this.parent.addModelSpecificFiltering(query); + this.parent.addModelSpecificFiltering(query, false, [this.table]); query.groupBy(1).orderBy('value', 'desc'); return query; } @@ -224,13 +206,14 @@ export class FieldInfo { ]); let query = queryDefinition.generateBaseQuery(true); this.parent.addFacetConstraints(query, this.parent.filteringFacets, this.name); - this.parent.addModelSpecificFiltering(query); + this.parent.addModelSpecificFiltering(query, false, [this.table]); if (this.log) { query.where(this.dataString(), ">", 0); } else { query.whereNotNull(this.dataString()); } query.groupBy('bin'); + // console.log(query.toString()); return query; } diff --git a/src/models/IBuildable.ts b/src/models/IBuildable.ts index f6ce1d7..a0dc98a 100644 --- a/src/models/IBuildable.ts +++ b/src/models/IBuildable.ts @@ -1,6 +1,7 @@ import {DatabaseConfig} from "./databaseConfig"; import * as Knex from "knex"; import {FieldInfo} from "./FieldInfo"; +import {SqlTable} from "./sqlTable"; export interface IBuildable { database: Knex; @@ -12,4 +13,5 @@ export interface IBuildable { ppiConfidence: number; getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string; + tableNeedsInnerJoin(sqlTable: SqlTable): boolean; } diff --git a/src/models/config.ts b/src/models/config.ts deleted file mode 100644 index 084afa4..0000000 --- a/src/models/config.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {FieldInfo} from "./FieldInfo"; -import {QueryDefinition} from "./queryDefinition"; - -/** - * Class for gathering tables and columns to use for standard queries - */ -export class Config { - /** - * TODO : get rid of this when the normal list pages are using the pharos_config.fieldList - * @param fieldKey - * @param sortTable - * @param sortColumn - * @constructor - */ - static GetDataFieldsFromKey(fieldKey: ConfigKeys, sortTable: string = "", sortColumn: string = ""): FieldInfo[] { - const dataFields: FieldInfo[] = []; - switch (fieldKey) { - case ConfigKeys.Target_List_Similarity: - break; - case ConfigKeys.Disease_List_Default: - dataFields.push({table: "disease", column: "ncats_name", alias: "name"} as FieldInfo); - break; - case ConfigKeys.Ligand_List_Default: - dataFields.push({table: "ncats_ligands", column: "identifier", alias: "ligid"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "isDrug", alias: "isdrug"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "name"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "smiles"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "actCnt", alias: "actcnt"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "PubChem"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "ChEMBL"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "Guide to Pharmacology"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "DrugCentral"} as FieldInfo); - dataFields.push({table: "ncats_ligands", column: "description"} as FieldInfo); - break; - } - if (sortTable && sortColumn && !dataFields.find(field => { - return field.column === sortColumn && field.table === sortTable - }) && !dataFields.find(field => { - return field.alias === sortColumn && field.table === sortTable - })) { - dataFields.push({table: sortTable, column: sortColumn, alias: sortColumn, group_method:"max"} as FieldInfo); - } else if (sortColumn && !dataFields.find(field => { - return field.column === sortColumn - }) && !dataFields.find(field => { - return field.alias === sortColumn - })) { - dataFields.push({table: sortTable, column: sortColumn, alias: sortColumn, group_method:"max"} as FieldInfo); - } - return dataFields; - } -} - -export enum ConfigKeys { - Target_List_Default, - Target_List_PPI, - Target_List_Disease, - Target_List_Similarity, - Disease_List_Default, - Ligand_List_Default -} diff --git a/src/models/databaseConfig.ts b/src/models/databaseConfig.ts index 171a8a1..0b628f6 100644 --- a/src/models/databaseConfig.ts +++ b/src/models/databaseConfig.ts @@ -1,6 +1,17 @@ import {DatabaseTable, TableLink, TableLinkInfo} from "./databaseTable"; import {FieldInfo} from "./FieldInfo"; +export class ModelInfo { + name: string; + table: string; + column: string; + constructor(obj: {name: string, table: string, column: string}) { + this.name = obj.name; + this.table = obj.table; + this.column = obj.column; + } +} + export class FieldList { model: string; @@ -20,11 +31,14 @@ export class FieldList { } static parseJsonContextFree(obj: any): FieldList { + if(obj.context === 'facet'){ + return new FieldList(obj.model, obj.context, obj.associatedModel, ''); + } return new FieldList(obj.model, '', obj.associatedModel, ''); } get listKey(): string { - return `${this.model}-${this.context}-${this.associatedModel}-${this.listName}`.toLowerCase(); + return `${this.model}-${this.context}-${this.associatedModel}`.toLowerCase() + `-${this.listName}`; } } @@ -36,8 +50,35 @@ export class DatabaseConfig { availableFieldMap: Map = new Map(); getAvailableFields(model: string, context: string = '', associatedModel: string = '', listName: string = ''): FieldInfo[] { - const key = new FieldList(model, context, associatedModel, listName).listKey; - return this.availableFieldMap.get(key)?.map(each => each.copy()) || []; + if(context === 'facet'){ // for facets, only the the exact match + const key = new FieldList(model, context, associatedModel, '').listKey; + const list = this.availableFieldMap.get(key)?.map(each => each.copy()); + if (list && list.length > 0) { + return list; + } + } + else if(context === 'list'){ // for lists, fall back on the model free lists + const key = new FieldList(model, context, associatedModel, '').listKey; + const list = this.availableFieldMap.get(key)?.map(each => each.copy()); + if (list && list.length > 0) { + return list; + } + if(associatedModel){ + const key = new FieldList(model, context, '', '').listKey; + const list = this.availableFieldMap.get(key)?.map(each => each.copy()); + if (list && list.length > 0) { + return list; + } + } + } + else if(context === 'download') { // for downloads, get the full lists, across all the download groups + const key = new FieldList(model, '', associatedModel, '').listKey; + const list = this.availableFieldMap.get(key)?.map(each => each.copy()); + if (list && list.length > 0) { + return list; + } + } + return []; } getDefaultFields(model: string, context: string = '', associatedModel: string = '', listName: string = ''): FieldInfo[] { @@ -46,11 +87,14 @@ export class DatabaseConfig { } getOneField(model: string, context: string = '', associatedModel: string, listName: string, name: string): FieldInfo | undefined { - const list = this.getAvailableFields(model, context, associatedModel, listName); - if (list.length > 0) { - return list.find((fieldInfo: FieldInfo) => fieldInfo.name === name)?.copy(); + let list = this.getAvailableFields(model, context, associatedModel, listName); + let match = list.find((fieldInfo: FieldInfo) => fieldInfo.name === name); + if (match) { + return match.copy(); } - return undefined; + list = this.getAvailableFields(model, 'download', '', 'Single Value Fields'); + match = list.find((fieldInfo: FieldInfo) => fieldInfo.name === name); + return match?.copy(); } populateFieldLists() { @@ -109,7 +153,7 @@ export class DatabaseConfig { const fieldInfo = new FieldInfo(row); if (this.availableFieldMap.has(key)) { const existingList = this.availableFieldMap.get(key) || []; - if(!existingList.map(f => f.name).includes(row.name)) { + if (!existingList.map(f => f.name).includes(row.name)) { existingList.push(fieldInfo); } } else { @@ -121,7 +165,7 @@ export class DatabaseConfig { } - modelList: Map = new Map(); + modelList: Map = new Map(); database: any; dbName: string; configDB: string; @@ -168,7 +212,7 @@ export class DatabaseConfig { loadModelMap() { let query = this.database({...this.modelTable}).select('*').then((rows: any[]) => { rows.forEach(row => { - this.modelList.set(row.table, row); + this.modelList.set(row.name, new ModelInfo(row)); }); }) } @@ -192,6 +236,13 @@ export class DatabaseConfig { } private _getLinkInformation(fromTable: string, toTable: string, reverse: boolean = false): TableLinkInfo | null { + if (fromTable === toTable) { + const selfTable = this.tables.find(table => table.tableName === fromTable); + if (!selfTable || !selfTable.primaryKey) { + return null; + } + return new TableLinkInfo(selfTable.primaryKey, selfTable.primaryKey); + } const from: DatabaseTable | undefined = this.tables.find((table: DatabaseTable) => { return table.tableName == fromTable; }); diff --git a/src/models/databaseTable.ts b/src/models/databaseTable.ts index 841c75d..e35cf8b 100644 --- a/src/models/databaseTable.ts +++ b/src/models/databaseTable.ts @@ -62,15 +62,19 @@ export class DatabaseTable { static requiredLinks: Map = new Map( [ + ["ncats_disease-disease", ["ncats_d2da"]], + ["ncats_disease-target", ["t2tc", "protein", "disease", "ncats_d2da"]], + ["ncats_disease-protein", ["disease", "ncats_d2da"]], + ["disease-target", ["t2tc", "protein"]], ["protein-viral_protein", ["viral_ppi"]], - // checked + ["protein-virus", ["viral_protein", "viral_ppi"]], ["protein-dto", ["p2dto"]], ["protein-panther_class", ["p2pc"]], ["protein-target", ["t2tc"]], ["protein-ncats_idg_list_type", ["ncats_idg_list"]], - ["protein-ncats_ligands", ["t2tc", "target", "ncats_ligand_activity"]], - ["protein-ncats_ligand_activity", ["t2tc", "target"]] + ["protein-ncats_ligands", ["ncats_ligand_activity", "target", "t2tc"]], + ["protein-ncats_ligand_activity", ["target", "t2tc"]] ]); static getRequiredLinks(table1: string, table2: string): string[] | undefined { diff --git a/src/models/disease/diseaseFacetFactory.ts b/src/models/disease/diseaseFacetFactory.ts deleted file mode 100644 index c4298a1..0000000 --- a/src/models/disease/diseaseFacetFactory.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {FacetFactory} from "../FacetFactory"; -import {DataModelList} from "../DataModelList"; -import {FieldInfo} from "../FieldInfo"; - -export class DiseaseFacetFactory extends FacetFactory { - GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FieldInfo { - const fieldInfo = parent.databaseConfig.getOneField('Disease', 'facet', parent.getAssociatedModel(), '', typeName); - if(!fieldInfo){ - return this.unknownFacet(); - } - - switch (typeName) { - case "Data Source": - fieldInfo.typeModifier = parent.associatedTarget; - fieldInfo.where_clause = this.getdiseaseWhereClause(parent.associatedTarget) || fieldInfo.where_clause; - case "Drug": - fieldInfo.typeModifier = parent.associatedTarget; - fieldInfo.where_clause = this.getdiseaseWhereClause(parent.associatedTarget, "drug_name is not null") || fieldInfo.where_clause; - } - return fieldInfo; - } - - getdiseaseWhereClause(sym: string, extraClause?: string) { - if (!sym) { - return extraClause; - } - let baseClause = `disease.protein_id = (select id from protein where MATCH (uniprot , sym , stringid) AGAINST ('${sym}' IN BOOLEAN MODE))`; - if (extraClause) { - return `${baseClause} and ${extraClause}`; - } - return baseClause; - } -} diff --git a/src/models/disease/diseaseList.ts b/src/models/disease/diseaseList.ts index c63da6a..a48e0b4 100644 --- a/src/models/disease/diseaseList.ts +++ b/src/models/disease/diseaseList.ts @@ -1,7 +1,6 @@ import {DataModelList} from "../DataModelList"; import {FieldInfo} from "../FieldInfo"; -import {DiseaseFacetFactory} from "./diseaseFacetFactory"; -import {ConfigKeys} from "../config"; +import {SqlTable} from "../sqlTable"; export class DiseaseList extends DataModelList { @@ -50,7 +49,7 @@ export class DiseaseList extends DataModelList { } constructor(tcrd: any, json: any) { - super(tcrd, "disease", "ncats_name", new DiseaseFacetFactory(), json); + super(tcrd, 'Disease', json); let facetList: string[]; if (this.associatedTarget) { @@ -71,31 +70,63 @@ export class DiseaseList extends DataModelList { return [{column: 'count', order: 'desc'}] }; - getDefaultFields() : FieldInfo[] { - return []; - // return ConfigKeys.Disease_List_Default - }; - - addModelSpecificFiltering(query: any): void { + getAvailableListFields() : FieldInfo[] { if (this.associatedTarget) { - query.join(this.getAssociatedTargetQuery().as('assocTarget'), 'assocTarget.name', this.keyString()); + return this.databaseConfig.getAvailableFields('Disease', 'list', 'Target'); } + return this.databaseConfig.getAvailableFields('Disease', 'list'); + }; + + addModelSpecificFiltering(query: any, list: boolean, tables: string[]): void { if (this.term.length > 0) { - query.andWhere(this.database.raw(`match(disease.ncats_name, disease.description, disease.drug_name) against('${this.term}*' in boolean mode)`)); + query.join(this.getTermQuery().as('termSearch'), 'termSearch.id', this.keyString()); + } + if (this.associatedTarget) { + if (!tables.includes('protein') && !tables.includes('target') && !tables.includes('disease')) { + query.join(this.getAssociatedTargetQuery().as('assocTarget'), 'assocTarget.name', this.keyString()); + } } } + getTermQuery(){ + return this.database({ncats_disease: 'ncats_disease', ncats_d2da: 'ncats_d2da', disease: 'disease'}) + .distinct({id: 'ncats_disease.id'}) + .whereRaw(`match(disease.ncats_name, disease.description, disease.drug_name) against('${this.term}*' in boolean mode)`) + .andWhere('ncats_disease.id', this.database.raw('ncats_d2da.ncats_disease_id')) + .andWhere('ncats_d2da.disease_assoc_id', this.database.raw('disease.id')); + } + getAssociatedTargetQuery(): any { - return this.database({disease: "disease", protein: "protein"}) + return this.database({ncats_disease: 'ncats_disease', ncats_d2da: 'ncats_d2da', disease: 'disease', protein: 'protein'}) .distinct({name: this.keyString()}).count('* as associationCount') .whereRaw(this.database.raw(`match(uniprot,sym,stringid) against('${this.associatedTarget}' in boolean mode)`)) - .andWhere(this.database.raw(`disease.protein_id = protein.id`)) + .andWhere('ncats_disease.id', this.database.raw('ncats_d2da.ncats_disease_id')) + .andWhere('ncats_d2da.disease_assoc_id', this.database.raw('disease.id')) + .andWhere('disease.protein_id', this.database.raw(`protein.id`)) .groupBy('name') .orderBy("associationCount", "desc"); } + tableNeedsInnerJoin(sqlTable: SqlTable) { + if (this.associatedTarget && (sqlTable.tableName === 'protein' || sqlTable.tableName === 'target' || sqlTable.tableName === 'disease')) { + return true; + } + return false; + } + getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string { - return ''; + if (this.associatedTarget && ( + fieldInfo.table === 'protein' || rootTableOverride === 'protein' || + fieldInfo.table === 'target' || rootTableOverride === 'target' || + fieldInfo.table === 'disease' || rootTableOverride === 'disease')) { + const modifiedFacet = this.facetsToFetch.find(f => f.name === fieldInfo.name); + if(modifiedFacet) { + modifiedFacet.typeModifier = this.associatedTarget; + } + return `disease.protein_id = (select id from protein where MATCH (uniprot , sym , stringid) AGAINST ('${this.associatedTarget}' IN BOOLEAN MODE))`; + } + return ""; } + } diff --git a/src/models/ligand/ligandFacetFactory.ts b/src/models/ligand/ligandFacetFactory.ts deleted file mode 100644 index 85ded97..0000000 --- a/src/models/ligand/ligandFacetFactory.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {FacetFactory} from "../FacetFactory"; -import {DataModelList} from "../DataModelList"; -import {FieldInfo} from "../FieldInfo"; - -export class LigandFacetFactory extends FacetFactory { - GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], extra?: any): FieldInfo { - const fieldInfo = parent.databaseConfig.getOneField('Disease', 'facet', parent.getAssociatedModel(), '', typeName); - if(!fieldInfo){ - return this.unknownFacet(); - } - - switch (typeName) { - case "Activity": - fieldInfo.typeModifier = parent.associatedTarget; - fieldInfo.where_clause = this.getActivityWhereClause(parent.associatedTarget, "act_type not in ('','-')") || fieldInfo.where_clause; - case "Action": - fieldInfo.typeModifier = parent.associatedTarget; - fieldInfo.where_clause = this.getActivityWhereClause(parent.associatedTarget, "action_type is not null") || fieldInfo.where_clause; - case "EC50": - case "IC50": - case "Kd": - case "Ki": - if (!parent.associatedTarget) { - return this.unknownFacet(); - } - fieldInfo.typeModifier = parent.associatedTarget; - fieldInfo.where_clause = this.getActivityWhereClause(parent.associatedTarget, `act_type = '${typeName}'`) || fieldInfo.where_clause; - } - return fieldInfo; - } - - getActivityWhereClause(sym: string, extraClause?: string){ - if (!sym) { - return extraClause; - } - let baseClause = `ncats_ligand_activity.target_id = (SELECT t2tc.target_id FROM protein, t2tc - WHERE MATCH (uniprot , sym , stringid) - AGAINST ('${sym}' IN BOOLEAN MODE) AND t2tc.protein_id = protein.id)`; - if(extraClause) { - return `${baseClause} and ${extraClause}`; - } - return baseClause; - } -} diff --git a/src/models/ligand/ligandList.ts b/src/models/ligand/ligandList.ts index 5cb5c33..76e913a 100644 --- a/src/models/ligand/ligandList.ts +++ b/src/models/ligand/ligandList.ts @@ -1,11 +1,10 @@ import {DataModelList} from "../DataModelList"; import {FieldInfo} from "../FieldInfo"; -import {LigandFacetFactory} from "./ligandFacetFactory"; -import {ConfigKeys} from "../config"; +import {SqlTable} from "../sqlTable"; -export class LigandList extends DataModelList{ +export class LigandList extends DataModelList { - static getAutocomplete(knex: any, term: string){ + static getAutocomplete(knex: any, term: string) { let query = knex("ncats_ligands") .select({value: knex.raw('distinct name'), source: knex.raw("'Ligand'")}) .where('name', 'not like', 'chembl%') @@ -18,7 +17,7 @@ export class LigandList extends DataModelList{ } constructor(tcrd: any, json: any) { - super(tcrd, "ncats_ligands" , "id", new LigandFacetFactory(), json); + super(tcrd, 'Ligand', json); let facetList: string[]; if (this.associatedTarget) { facetList = this.DefaultFacetsWithTarget; @@ -34,33 +33,57 @@ export class LigandList extends DataModelList{ .map(a => a.name) || []; }; - defaultSortParameters(): {column: string; order: string}[] - { + defaultSortParameters(): { column: string; order: string }[] { return [{column: 'actcnt', order: 'desc'}]; }; - getDefaultFields(): FieldInfo[] { - return []; - // return ConfigKeys.Ligand_List_Default - }; - - addModelSpecificFiltering(query: any): void { - if(this.associatedTarget){ - let associatedTargetQuery = this.database({ncats_ligands: "ncats_ligands", ncats_ligand_activity: "ncats_ligand_activity", t2tc: "t2tc", protein: "protein"}) - .distinct("ncats_ligands.identifier") - .whereRaw(this.database.raw(`match(uniprot,sym,stringid) against('${this.associatedTarget}' in boolean mode)`)) - .andWhere(this.database.raw(`ncats_ligands.id = ncats_ligand_activity.ncats_ligand_id`)) - .andWhere(this.database.raw(`ncats_ligand_activity.target_id = t2tc.target_id`)) - .andWhere(this.database.raw(`t2tc.protein_id = protein.id`)).as('assocTarget'); - query.join(associatedTargetQuery, 'assocTarget.identifier', 'ncats_ligands.identifier'); + getAvailableListFields(): FieldInfo[] { + if (this.associatedTarget) { + const fieldList = this.databaseConfig.getAvailableFields('Ligand', 'list', 'Target'); + return fieldList; } - else if (this.term.length > 0){ + return this.databaseConfig.getAvailableFields('Ligand', 'list'); + } + + addModelSpecificFiltering(query: any, list: boolean, tables: string[]): void { + if (this.associatedTarget) { + if(!tables.includes('ncats_ligand_activity')) { + let associatedTargetQuery = this.database({ + ncats_ligands: "ncats_ligands", + ncats_ligand_activity: "ncats_ligand_activity", + t2tc: "t2tc", + protein: "protein" + }) + .distinct("ncats_ligands.identifier") + .whereRaw(this.database.raw(`match(uniprot,sym,stringid) against('${this.associatedTarget}' in boolean mode)`)) + .andWhere(this.database.raw(`ncats_ligands.id = ncats_ligand_activity.ncats_ligand_id`)) + .andWhere(this.database.raw(`ncats_ligand_activity.target_id = t2tc.target_id`)) + .andWhere(this.database.raw(`t2tc.protein_id = protein.id`)).as('assocTarget'); + query.join(associatedTargetQuery, 'assocTarget.identifier', 'ncats_ligands.identifier'); + } + } else if (this.term.length > 0) { query.whereRaw(`match(name, ChEMBL, PubChem, \`Guide to Pharmacology\`, DrugCentral) against('${this.term}*')`); } } - getSpecialModelWhereClause(fieldInfo: FieldInfo, rootTableOverride: string): string { - return ''; + tableNeedsInnerJoin(sqlTable: SqlTable) { + return false; + } + + getSpecialModelWhereClause( fieldInfo: FieldInfo, rootTableOverride: string): string { + if (this.associatedTarget && (fieldInfo.table === 'ncats_ligand_activity' || rootTableOverride === 'ncats_ligand_activity')) { + const modifiedFacet = this.facetsToFetch.find(f => f.name === fieldInfo.name); + if(modifiedFacet) { + modifiedFacet.typeModifier = this.associatedTarget; + } + return `ncats_ligand_activity.target_id = ( + SELECT t2tc.target_id + FROM protein, t2tc + WHERE MATCH (uniprot , sym , stringid) + AGAINST ('${this.associatedTarget}' IN BOOLEAN MODE) + AND t2tc.protein_id = protein.id)`; + } + return ""; } } diff --git a/src/models/queryDefinition.ts b/src/models/queryDefinition.ts index 299e278..6a87055 100644 --- a/src/models/queryDefinition.ts +++ b/src/models/queryDefinition.ts @@ -8,8 +8,9 @@ import {IBuildable} from "./IBuildable"; */ export class QueryDefinition { buildable: IBuildable; + dataList: FieldInfo[]; static GenerateQueryDefinition(buildabelObj: IBuildable, dataList: FieldInfo[]): QueryDefinition { - let qd = new QueryDefinition(buildabelObj); + let qd = new QueryDefinition(buildabelObj, dataList); for (let i = 0; i < dataList.length; i++) { qd.addRequestedData(dataList[i]); } @@ -18,12 +19,21 @@ export class QueryDefinition { tables: SqlTable[] = []; - private constructor(buildableObj: IBuildable) { + private constructor(buildableObj: IBuildable, dataList: FieldInfo[]) { + this.dataList = dataList; this.buildable = buildableObj; } addRequestedData(reqData: FieldInfo) { - reqData.where_clause = this.buildable.getSpecialModelWhereClause(reqData, this.buildable.rootTable) || reqData.where_clause; + const specialWhereClause = this.buildable.getSpecialModelWhereClause(reqData, this.buildable.rootTable); + if(specialWhereClause){ + if(reqData.where_clause){ + reqData.where_clause = reqData.where_clause + ' and ' + specialWhereClause; + } + else { + reqData.where_clause = specialWhereClause; + } + } const existingTable = this.tables.find(table => { return table.equals(reqData.table, reqData.where_clause); }); @@ -43,7 +53,7 @@ export class QueryDefinition { let tableAlias = reqData.table; if(tableCount > 0){ tableAlias = reqData.table + tableCount; - const re = new RegExp('\\b' + reqData.table + '\\b', 'i'); + const re = new RegExp('\\b' + reqData.table + '\\b', 'ig'); reqData.where_clause = reqData.where_clause?.replace(re, tableAlias); reqData.select = reqData.select?.replace(re, tableAlias); } @@ -103,7 +113,7 @@ export class QueryDefinition { const columnName = column.alias || column.column; if (column.group_method) { columnList[columnName] = db.raw(column.group_method + `(distinct ${select})`); - } else if (column.needsDistinct){ + } else if (column.needsDistinct) { columnList[columnName] = db.raw(`distinct ${select}`); } else { columnList[columnName] = db.raw(select); @@ -137,33 +147,40 @@ export class QueryDefinition { let query = buildableObj.database(rootTableObject.tableName) .select(this.getColumnList(buildableObj.database)); - let joinFunction = 'leftJoin'; - if(forFacet){ - joinFunction = 'join'; - } + const tablesInQuery: string[] = []; joinTables.forEach(dataTable => { if(dataTable.columns[0].isFromListQuery) { return; } + let joinFunction = 'leftJoin'; + if(forFacet || this.buildable.tableNeedsInnerJoin(dataTable)){ + joinFunction = 'join'; + } let leftTable = rootTableObject; dataTable.linkingTables.forEach(linkTableName => { - let linkInfo = buildableObj.databaseConfig.getLinkInformation(leftTable.tableName, linkTableName); - // @ts-ignore - query[joinFunction](linkTableName, function (this: any) { - this.on(`${leftTable.alias}.${linkInfo.fromCol}`, `=`, `${linkTableName}.${linkInfo.toCol}`); - }); + if(!tablesInQuery.includes(linkTableName)) { + let linkInfo = buildableObj.databaseConfig.getLinkInformation(leftTable.tableName, linkTableName); + // @ts-ignore + query[joinFunction](linkTableName, function (this: any) { + this.on(`${leftTable.alias}.${linkInfo.fromCol}`, `=`, `${linkTableName}.${linkInfo.toCol}`); + }); + tablesInQuery.push(linkTableName); + } leftTable = new SqlTable(linkTableName); }); - let linkInfo = buildableObj.databaseConfig.getLinkInformation(leftTable.tableName, dataTable.tableName); - // @ts-ignore - query[joinFunction]({[dataTable.alias]: dataTable.tableName}, function (this: any) { - this.on(`${leftTable.alias}.${linkInfo.fromCol}`, `=`, `${dataTable.alias}.${linkInfo.toCol}`); - if(dataTable.joinConstraint){ - this.andOn(buildableObj.database.raw(dataTable.joinConstraint)); - } - }); + if(!tablesInQuery.includes(dataTable.alias)) { + let linkInfo = buildableObj.databaseConfig.getLinkInformation(leftTable.tableName, dataTable.tableName); + // @ts-ignore + query[joinFunction]({[dataTable.alias]: dataTable.tableName}, function (this: any) { + this.on(`${leftTable.alias}.${linkInfo.fromCol}`, `=`, `${dataTable.alias}.${linkInfo.toCol}`); + if (dataTable.joinConstraint) { + this.andOn(buildableObj.database.raw(dataTable.joinConstraint)); + } + }); + tablesInQuery.push(dataTable.alias); + } }); return query; } diff --git a/src/models/sqlTable.ts b/src/models/sqlTable.ts index fb9343a..887cdbd 100644 --- a/src/models/sqlTable.ts +++ b/src/models/sqlTable.ts @@ -14,16 +14,13 @@ export class SqlTable { constructor(tableName: string, {alias = "", joinConstraint = ""} = {}, linkingTables: string[] = []) { + if (alias) { this._alias = alias; } this.tableName = tableName; this.joinConstraint = joinConstraint; - this.linkingTables = linkingTables; - if (alias) { - this._alias = alias; - } } equals(tableName: string, joinConstraint: string | undefined) { diff --git a/src/models/target/targetFacetFactory.ts b/src/models/target/targetFacetFactory.ts deleted file mode 100644 index 54a5c10..0000000 --- a/src/models/target/targetFacetFactory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {DataModelList} from "../DataModelList"; -import {FieldInfo} from "../FieldInfo"; -import {FacetFactory} from "../FacetFactory"; - -export class TargetFacetFactory extends FacetFactory { - - GetFacet(parent: DataModelList, typeName: string, allowedValues: string[], nullQuery: boolean = false): FieldInfo { - const fieldInfo = parent.databaseConfig.getOneField('Target', 'facet', parent.getAssociatedModel(), '', typeName); - if(!fieldInfo){ - return this.unknownFacet(); - } - fieldInfo.allowedValues = allowedValues; - fieldInfo.parent = parent; - if (typeName === "PPI Data Source") { // :( - fieldInfo.valuesDelimited = true; - } - return fieldInfo; - } -} diff --git a/src/models/target/targetList.ts b/src/models/target/targetList.ts index 0adb95d..798df74 100644 --- a/src/models/target/targetList.ts +++ b/src/models/target/targetList.ts @@ -1,11 +1,9 @@ -import {TargetFacetFactory} from "./targetFacetFactory"; import {DataModelList} from "../DataModelList"; import {FieldInfo} from "../FieldInfo"; import {Jaccard} from "../similarTargets/jaccard"; +import {SqlTable} from "../sqlTable"; export class TargetList extends DataModelList { - skip: number = 0; - top: number = 10; proteinList: string[] = []; proteinListCached: boolean = false; @@ -14,13 +12,13 @@ export class TargetList extends DataModelList { return [{column: 'id', order: 'asc'}]; } if (this.term) { - return [{column: 'min_score', order: 'asc'}, {column: 'name', order: 'asc'}]; + return [{column: 'search_score', order: 'asc'}, {column: 'name', order: 'asc'}]; } if (this.associatedTarget) { return [{column: 'p_int', order: 'desc'}, {column: 'score', order: 'desc'}]; } if (this.associatedDisease) { - return [{column: 'dtype', order: 'desc'}]; + return [{column: 'datasource_count', order: 'desc'}]; } if (this.similarity.match.length > 0) { return [{column: 'jaccard', order: 'desc'}]; @@ -29,7 +27,7 @@ export class TargetList extends DataModelList { } constructor(tcrd: any, json: any) { - super(tcrd, "protein", "id", new TargetFacetFactory(), json); + super(tcrd, 'Target', json); let facetList: string[]; if (!json || !json.facets || json.facets.length == 0) { @@ -54,7 +52,7 @@ export class TargetList extends DataModelList { } } - getDefaultFields(): FieldInfo[] { + getAvailableListFields(): FieldInfo[] { if (this.associatedTarget) { return this.databaseConfig.getAvailableFields('Target', 'list', 'Target'); } @@ -77,7 +75,7 @@ export class TargetList extends DataModelList { return dataFields; } - addModelSpecificFiltering(query: any, list: boolean = false): void { + addModelSpecificFiltering(query: any, list: boolean = false, tables: string[]): void { let filterQuery; if (list) { if(this.term.length > 0){ @@ -87,10 +85,14 @@ export class TargetList extends DataModelList { filterQuery = this.getSimilarityQuery(); } else if(this.associatedDisease.length > 0) { - filterQuery = this.fetchProteinList(); + if(!tables.includes('disease')) { + filterQuery = this.fetchProteinList(); + } } else if(this.associatedTarget.length > 0) { - filterQuery = this.fetchProteinList(); + if (!tables.includes('ncats_ppi')) { + filterQuery = this.fetchProteinList(); + } } } else { filterQuery = this.fetchProteinList(); @@ -147,7 +149,7 @@ export class TargetList extends DataModelList { distinct protein_id FROM disease as d -JOIN (SELECT "${this.associatedDisease}" AS name UNION SELECT +JOIN (SELECT lst.name FROM ncats_do lst, @@ -158,7 +160,7 @@ JOIN (SELECT "${this.associatedDisease}" AS name UNION SELECT WHERE name = "${this.associatedDisease}") AS finder WHERE - finder.lft + 1 <= lst.lft + finder.lft <= lst.lft AND finder.rght >= lst.rght) as diseaseList ON diseaseList.name = d.ncats_name`)); } @@ -187,11 +189,44 @@ ON diseaseList.name = d.ncats_name`)); .map(a => a.name) || []; }; + tableNeedsInnerJoin(sqlTable: SqlTable) { + if (this.associatedDisease && sqlTable.tableName === 'disease'){ + return true; + } + if (this.associatedTarget && sqlTable.tableName === 'ncats_ppi'){ + return true; + } + return false; + } + getSpecialModelWhereClause( fieldInfo: FieldInfo, rootTableOverride: string): string { if (this.associatedTarget && (fieldInfo.table === 'ncats_ppi' || rootTableOverride === 'ncats_ppi')) { + const modifiedFacet = this.facetsToFetch.find(f => f.name === fieldInfo.name); + if(modifiedFacet) { + modifiedFacet.typeModifier = this.associatedTarget; + } return `ncats_ppi.other_id = (select id from protein where match(uniprot,sym,stringid) against('${this.associatedTarget}' in boolean mode)) and NOT (ncats_ppi.ppitypes = 'STRINGDB' AND ncats_ppi.score < ${this.ppiConfidence})`; } + if (this.associatedDisease && (fieldInfo.table === 'disease' || rootTableOverride === 'disease')) { + const modifiedFacet = this.facetsToFetch.find(f => f.name === fieldInfo.name); + if(modifiedFacet) { + modifiedFacet.typeModifier = this.associatedDisease; + } + return `disease.ncats_name in ( + SELECT + lst.name + FROM + ncats_do lst, (SELECT + MIN(lft) AS 'lft', MIN(rght) AS 'rght' + FROM + ncats_do + WHERE + name = '${this.associatedDisease}') AS finder + WHERE + finder.lft <= lst.lft + AND finder.rght + 0 >= lst.rght)`; + } return ""; } } diff --git a/src/resolvers.js b/src/resolvers.js index 8ebebc5..f5e771c 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -10,34 +10,52 @@ const {find, filter, slice} = require('lodash'); const resolvers = { FieldList: { - listName: async function(listObject, args, {dataSources}){ - return listObject; + listName: async function (listObject, args, {dataSources}) { + const pieces = listObject.split('-'); + return pieces[pieces.length - 1]; }, - field: async function(listObject, args, {dataSources}){ - return dataSources.tcrd.tableInfo.fieldLists.get(listObject); + field: async function (listObject, args, {dataSources}) { + return dataSources.tcrd.tableInfo.availableFieldMap.get(listObject); } }, PharosConfiguration: { - lists: async function(config, args, {dataSources}){ - if(args.listNames){ + lists: async function (config, args, {dataSources}) { + if (args.listNames) { return [...args.listNames]; } - return config.fieldLists.keys(); - }, - downloadLists: async function(config, args, {dataSources}){ - return Array.from(config.fieldLists.keys()).filter(key => key.startsWith(args.modelName + ' Field Group')); + return config.availableFieldMap.keys(); + }, + downloadLists: async function (config, args, {dataSources}) { + console.log(args); + return Array.from(config.availableFieldMap.keys()).filter(listKey => { + const pieces = listKey.split('-'); + const reqModel = args.modelName ? args.modelName.toLowerCase() : ''; + const reqAssocModel = args.associatedModelName ? args.associatedModelName.toLowerCase() : ''; + if (pieces[1] != 'download'){ // must be download lists + return false; + } + if (pieces[0] != reqModel) { // must match the model + return false; + } + if (pieces[2] == ''){ // normal fields should apply no matter the associated model + return true; + } + if (pieces[2] == reqAssocModel) { // otherwise, have to have the right associated model + return true; + } + return false; + }); } }, Query: { - download: async function (_, args, {dataSources}){ + download: async function (_, args, {dataSources}) { let listQuery; try { const listObj = DataModelListFactory.getListObject(args.model, dataSources.tcrd, args); listQuery = listObj.getListQuery(); - } - catch(e){ + } catch (e) { return { result: false, errorDetails: e.message @@ -50,7 +68,7 @@ const resolvers = { }; }, - configuration: async function (_, args, {dataSources}){ + configuration: async function (_, args, {dataSources}) { return dataSources.tcrd.tableInfo; }, @@ -119,7 +137,7 @@ const resolvers = { }, targetFacets: async function (_, args, {dataSources}) { - return dataSources.tcrd.tableInfo.fieldLists.get('Target Facet - All').map(facet => facet.name); + return dataSources.tcrd.tableInfo.availableFieldMap.get('target-facet--').map(facet => facet.name); }, target: async function (_, args, {dataSources}) { @@ -247,16 +265,40 @@ const resolvers = { }, batch: async function (line, args, {dataSources}, info) { + const tryUnbatch = function () { + if (info && info.fieldNodes && info.fieldNodes.length > 0 && + info.fieldNodes[0].arguments && info.fieldNodes[0].arguments.length > 0 && + info.fieldNodes[0].arguments[0].name) { + return info.fieldNodes[0].arguments[0].name.value; + } + }; + const model = tryUnbatch(); + if(model == 'targets'){ + return getTargetResult(args, dataSources).then(res => { + return {targetResult: res}; + }) + } + if(model == 'diseases'){ + return getDiseaseResult(args, dataSources.tcrd).then(res => { + return {diseaseResult: res}; + }) + } + if(model == 'ligands'){ + return getLigandResult(args, dataSources.tcrd).then(res => { + return {ligandResult: res}; + }) + } + console.log('unbatching failed, executing the whole batch'); let funcs = [ getTargetResult(args, dataSources), - // getDiseaseResult(args, dataSources.tcrd), - // getLigandResult(args, dataSources.tcrd) + getDiseaseResult(args, dataSources.tcrd), + getLigandResult(args, dataSources.tcrd) ]; return Promise.all(funcs).then(r => { return { targetResult: r[0], - // diseaseResult: r[1], - // ligandResult: r[2] // sigh + diseaseResult: r[1], + ligandResult: r[2] // sigh }; }).catch(function (error) { console.error(error); @@ -464,7 +506,7 @@ const resolvers = { return rowData.tcrdid == target.tcrdid; }); if (theRow) { - theRow.score = theRow.score / 1000; + theRow.score = theRow.score; return theRow; } } @@ -495,10 +537,10 @@ const resolvers = { diseaseArgs.filter = diseaseArgs.filter || {}; diseaseArgs.filter.associatedTarget = target.uniprot; let diseaseList = new DiseaseList(dataSources.tcrd, diseaseArgs); - const q = diseaseList.getAssociatedTargetQuery(); + const q = diseaseList.getListQuery(); return q.then(rows => { rows.forEach(x => { - x.value = x.associationCount; + x.value = x.count; }); return rows; }); @@ -509,13 +551,7 @@ const resolvers = { diseaseArgs.filter = diseaseArgs.filter || {}; diseaseArgs.filter.associatedTarget = target.uniprot; let diseaseList = new DiseaseList(dataSources.tcrd, diseaseArgs); - const q = diseaseList.getAssociatedTargetQuery(); - if (args.top) { - q.limit(args.top); - } - if (args.skip) { - q.offset(args.skip); - } + const q = diseaseList.getListQuery(); return q.then(rows => { let diseases = filter(rows, r => r.name != null && r.associationCount > 0); @@ -836,7 +872,8 @@ const resolvers = { } else { ligandArgs.filter.facets.push({facet: "Type", values: ["Ligand"]}); } - return new LigandList(dataSources.tcrd, ligandArgs).getListQuery() + return new LigandList(dataSources.tcrd, ligandArgs) + .getListQuery() .then(allResults => { return allResults; }) @@ -862,8 +899,8 @@ const resolvers = { console.error(error); }); }, - similarity: async function (target, args, {dataSources}){ - if(dataSources.similarity) { + similarity: async function (target, args, {dataSources}) { + if (dataSources.similarity) { return target; } return null; @@ -871,14 +908,14 @@ const resolvers = { sequence_variants: async function (target, args, {dataSources}) { const targetDetails = new TargetDetails(args, target, dataSources.tcrd); return targetDetails.getSequenceVariants().then(results => { - if(!results || results.length < 1){ + if (!results || results.length < 1) { return null; } const residueData = []; let currentResidue = []; let lastResidueIndex = -1; results.forEach(row => { - if(lastResidueIndex != row.residue){ + if (lastResidueIndex != row.residue) { lastResidueIndex = row.residue; currentResidue = []; residueData.push(currentResidue); @@ -897,19 +934,19 @@ const resolvers = { return results; }); }, - facetValueCount: async function(target, args, {dataSources}){ + facetValueCount: async function (target, args, {dataSources}) { if (!args.facetName) { return null; } const targetDetails = new TargetDetails(args, target, dataSources.tcrd); return targetDetails.getFacetValueCount().then(results => { - if(results && results.length > 0) { + if (results && results.length > 0) { return results[0].value; } return null; }); }, - facetValues: async function(target, args, {dataSources}) { + facetValues: async function (target, args, {dataSources}) { if (!args.facetName) { return null; } @@ -919,14 +956,14 @@ const resolvers = { }); } }, - SimilarityDetails:{ - commonOptions: async function (target, args, {dataSources}){ - if(target && target.commonOptions && dataSources.similarity) { + SimilarityDetails: { + commonOptions: async function (target, args, {dataSources}) { + if (target && target.commonOptions && dataSources.similarity) { const options = target.commonOptions.split('|'); - if(options.length <= 20){ + if (options.length <= 20) { return options; } - return [...options.slice(0,20), `...and ${target.overlap - 20} more`]; + return [...options.slice(0, 20), `...and ${target.overlap - 20} more`]; } return null; } @@ -1329,9 +1366,9 @@ const resolvers = { console.error(error); }); }, - similarityTarget: async function (target, args, {dataSources}){ - if(dataSources.similarity && dataSources.similarity.match) { - return resolvers.Query.target(null, {q: {uniprot :dataSources.similarity.match}}, {dataSources}); + similarityTarget: async function (target, args, {dataSources}) { + if (dataSources.similarity && dataSources.similarity.match) { + return resolvers.Query.target(null, {q: {uniprot: dataSources.similarity.match}}, {dataSources}); } return null; }, @@ -1623,7 +1660,7 @@ function getTargetResult(args, dataSources) { count: count, facets: facets }; - }).catch( error => { + }).catch(error => { console.error(error); }); } diff --git a/src/schema.graphql b/src/schema.graphql index 4c6fe4b..bd9c802 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -659,7 +659,7 @@ type DataSourceCount @cacheControl(maxAge: 604800){ type PharosConfiguration{ lists (listNames: [String]): [FieldList] - downloadLists (modelName: String): [FieldList] + downloadLists (modelName: String, associatedModelName: String): [FieldList] } type FieldList{ @@ -669,11 +669,14 @@ type FieldList{ type FieldDetails{ order: Int - type: String - dataTable: String - dataColumn: String + name: String + description: String + table: String + column: String + alias: String select: String - whereClause: String + where_clause: String + group_method: String null_table: String null_column: String null_count_column: String @@ -681,10 +684,7 @@ type FieldDetails{ dataType: String binSize: String log: String - sourceExplanation: String - modelName: String - rootTable: String - rootColumn: String + default: Boolean } type Query{ From 7c0f1e410a170cfb6eca9ef6550ad396963376cd Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 23 Mar 2021 10:47:44 -0400 Subject: [PATCH 03/19] polish up basic data download functionality --- app.yaml | 2 +- src/models/DataModelList.ts | 8 ++++---- src/models/disease/diseaseList.ts | 3 +++ src/models/ligand/ligandList.ts | 3 +++ src/models/target/targetList.ts | 5 ----- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app.yaml b/app.yaml index 6937040..82f5053 100644 --- a/app.yaml +++ b/app.yaml @@ -1,2 +1,2 @@ -instance_class: F4 +instance_class: F4_1G runtime: nodejs10 diff --git a/src/models/DataModelList.ts b/src/models/DataModelList.ts index cc52ef1..1deeb97 100644 --- a/src/models/DataModelList.ts +++ b/src/models/DataModelList.ts @@ -30,8 +30,8 @@ export abstract class DataModelList implements IBuildable { associatedDisease: string = ""; similarity: { match: string, facet: string } = {match: '', facet: ''}; ppiConfidence: number = CONSTANTS.DEFAULT_PPI_CONFIDENCE; - skip: number = 0; - top: number = 10; + skip: number | undefined; + top: number | undefined; modelInfo: ModelInfo = {name: '', table: '', column: ''}; tcrd: any; @@ -167,7 +167,7 @@ export abstract class DataModelList implements IBuildable { this.pushOneDataField(this.sortField, 'list', dataFields); } this.dataFields = dataFields; - const sortField = dataFields.find(f => f.name === this.sortField); + const sortField = dataFields.find(f => f.name.length > 0 && f.name === this.sortField); const queryDefinition = QueryDefinition.GenerateQueryDefinition(this, dataFields); @@ -191,7 +191,7 @@ export abstract class DataModelList implements IBuildable { } addSort(query: any, queryDefinition: QueryDefinition, sortFieldInfo: FieldInfo | undefined) { - if (!this.sortField || !sortFieldInfo) { + if (!sortFieldInfo) { query.orderBy(this.defaultSortParameters()); return; } diff --git a/src/models/disease/diseaseList.ts b/src/models/disease/diseaseList.ts index a48e0b4..d582cea 100644 --- a/src/models/disease/diseaseList.ts +++ b/src/models/disease/diseaseList.ts @@ -67,6 +67,9 @@ export class DiseaseList extends DataModelList { }; defaultSortParameters(): {column: string; order: string}[] { + if (this.fields.length > 0) { + return [{column: 'id', order: 'asc'}]; + } return [{column: 'count', order: 'desc'}] }; diff --git a/src/models/ligand/ligandList.ts b/src/models/ligand/ligandList.ts index 76e913a..87875e0 100644 --- a/src/models/ligand/ligandList.ts +++ b/src/models/ligand/ligandList.ts @@ -34,6 +34,9 @@ export class LigandList extends DataModelList { }; defaultSortParameters(): { column: string; order: string }[] { + if (this.fields.length > 0) { + return [{column: 'id', order: 'asc'}]; + } return [{column: 'actcnt', order: 'desc'}]; }; diff --git a/src/models/target/targetList.ts b/src/models/target/targetList.ts index 798df74..7206429 100644 --- a/src/models/target/targetList.ts +++ b/src/models/target/targetList.ts @@ -45,11 +45,6 @@ export class TargetList extends DataModelList { } this.facetsToFetch = FieldInfo.deduplicate( this.facetsToFetch.concat(this.facetFactory.getFacetsFromList(this, facetList, this.isNull()))); - - if (json) { - this.skip = json.skip; - this.top = json.top; - } } getAvailableListFields(): FieldInfo[] { From 25c847cdc21f1816b5720380d77eae3fd298b977 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 23 Mar 2021 15:00:29 -0400 Subject: [PATCH 04/19] work with diseases and ligands --- src/models/databaseTable.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/databaseTable.ts b/src/models/databaseTable.ts index e35cf8b..1f006bb 100644 --- a/src/models/databaseTable.ts +++ b/src/models/databaseTable.ts @@ -62,6 +62,8 @@ export class DatabaseTable { static requiredLinks: Map = new Map( [ + ["ncats_disease-ncats_ligands", ["ncats_ligand_activity", "target", "t2tc", "protein", "disease", "ncats_d2da"]], + ["ncats_disease-ncats_ligand_activity", ["target", "t2tc", "protein", "disease", "ncats_d2da"]], ["ncats_disease-disease", ["ncats_d2da"]], ["ncats_disease-target", ["t2tc", "protein", "disease", "ncats_d2da"]], ["ncats_disease-protein", ["disease", "ncats_d2da"]], From f798b3ef8677303c1e02f27e6c511bdfb982becf Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 23 Mar 2021 17:34:23 -0400 Subject: [PATCH 05/19] work with diseases and ligands --- src/__tests__/targetQueries/getListQuery.test.js | 2 +- src/models/DataModelList.ts | 2 +- src/models/ligand/ligandList.ts | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/__tests__/targetQueries/getListQuery.test.js b/src/__tests__/targetQueries/getListQuery.test.js index 1a84b60..40a61a0 100644 --- a/src/__tests__/targetQueries/getListQuery.test.js +++ b/src/__tests__/targetQueries/getListQuery.test.js @@ -30,7 +30,7 @@ describe('list queries should work', function () { top: 10, fields: ["Family", "UniProt", "PDB IDs"] }); - console.log(listObj.getListQuery().toString()); + // console.log(listObj.getListQuery().toString()); return listObj.getListQuery().then(res => { expect(res.length).toBe(10); res.forEach(val => { diff --git a/src/models/DataModelList.ts b/src/models/DataModelList.ts index 1deeb97..bc7fb81 100644 --- a/src/models/DataModelList.ts +++ b/src/models/DataModelList.ts @@ -167,7 +167,7 @@ export abstract class DataModelList implements IBuildable { this.pushOneDataField(this.sortField, 'list', dataFields); } this.dataFields = dataFields; - const sortField = dataFields.find(f => f.name.length > 0 && f.name === this.sortField); + const sortField = dataFields.find(f => f.name && f.name.length > 0 && f.name === this.sortField); const queryDefinition = QueryDefinition.GenerateQueryDefinition(this, dataFields); diff --git a/src/models/ligand/ligandList.ts b/src/models/ligand/ligandList.ts index 87875e0..495e7a9 100644 --- a/src/models/ligand/ligandList.ts +++ b/src/models/ligand/ligandList.ts @@ -70,6 +70,9 @@ export class LigandList extends DataModelList { } tableNeedsInnerJoin(sqlTable: SqlTable) { + if (this.associatedTarget && (sqlTable.tableName === 'protein' || sqlTable.tableName === 'target' || sqlTable.tableName === 'ncats_ligand_activity')) { + return true; + } return false; } @@ -84,7 +87,7 @@ export class LigandList extends DataModelList { FROM protein, t2tc WHERE MATCH (uniprot , sym , stringid) AGAINST ('${this.associatedTarget}' IN BOOLEAN MODE) - AND t2tc.protein_id = protein.id)`; + AND t2tc.protein_id = protein.id)`; } return ""; } From 2945b849cd8fb7b8e4e495c168f2bca276d43d65 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 23 Mar 2021 18:16:14 -0400 Subject: [PATCH 06/19] fix similar target links --- src/models/databaseConfig.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/databaseConfig.ts b/src/models/databaseConfig.ts index 0b628f6..b291636 100644 --- a/src/models/databaseConfig.ts +++ b/src/models/databaseConfig.ts @@ -271,7 +271,7 @@ export class DatabaseConfig { let joinTables: string[] = []; if (dataTable != rootTable) { - const links = DatabaseTable.getRequiredLinks(dataTable, rootTable) || []; + const links = DatabaseTable.getRequiredLinks(dataTable, rootTable)?.slice().reverse() || []; joinTables.push(...links); joinTables.push(rootTable); } @@ -298,6 +298,7 @@ export class DatabaseConfig { if (facet.where_clause.length > 0) { query.whereRaw(facet.where_clause); } + // console.log(query.toString()); return query; } } From 3f967f409bebb010afa15dae60dcad93f637a63f Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 23 Mar 2021 23:10:28 -0400 Subject: [PATCH 07/19] fix disease component --- src/resolvers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/resolvers.js b/src/resolvers.js index f5e771c..32af343 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -552,10 +552,11 @@ const resolvers = { diseaseArgs.filter.associatedTarget = target.uniprot; let diseaseList = new DiseaseList(dataSources.tcrd, diseaseArgs); const q = diseaseList.getListQuery(); - return q.then(rows => { - let diseases = filter(rows, r => r.name != null - && r.associationCount > 0); - diseases.forEach(x => x.parent = target); + return q.then(diseases => { + diseases.forEach(x => { + x.associationCount = x.count; + x.parent = target; + }); return diseases; }).catch(function (error) { console.error(error); From 93d1c457e4d35ed7fd1b57239ee9824e4c0a3e3b Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Wed, 24 Mar 2021 01:56:24 -0400 Subject: [PATCH 08/19] delay startup --- src/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 0d163e1..5341ffc 100644 --- a/src/index.js +++ b/src/index.js @@ -72,10 +72,12 @@ server.applyMiddleware({ const PORT = process.env.PORT || 4000; -setTimeout(() => { - app.listen({port: PORT}, () => { - console.log('🏭 using configuration from: ' + cred.CONFIGDB); - console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`) - }); +tcrd.tableInfo.loadPromise.then(() => { + setTimeout(() => { + app.listen({port: PORT}, () => { + console.log('🏭 using configuration from: ' + cred.CONFIGDB); + console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`) + }); + }, 10000); }); From 469ce1f67c71f5f24fbe2cffb7d54452e59b8352 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Wed, 24 Mar 2021 14:58:00 -0400 Subject: [PATCH 09/19] load tables differently --- src/TCRD.js | 4 +++- src/index.js | 17 +++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/TCRD.js b/src/TCRD.js index fd06894..0c77ed4 100644 --- a/src/TCRD.js +++ b/src/TCRD.js @@ -109,7 +109,9 @@ class TCRD extends SQLDataSource { super(config); const _this = this; this.tableInfo = new DatabaseConfig(this.db, config.connection.database, config.connection.configDB); - + this.tableInfo.loadPromise.then(() => { + console.log('database tables loaded'); + }); const root = { doid: 'DOID:4', name: 'disease', diff --git a/src/index.js b/src/index.js index 5341ffc..4dc80e8 100644 --- a/src/index.js +++ b/src/index.js @@ -70,14 +70,11 @@ server.applyMiddleware({ path: '/graphql' }); -const PORT = process.env.PORT || 4000; - -tcrd.tableInfo.loadPromise.then(() => { - setTimeout(() => { - app.listen({port: PORT}, () => { - console.log('🏭 using configuration from: ' + cred.CONFIGDB); - console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`) - }); - }, 10000); -}); +const PORT = process.env.PORT || 4444; +setTimeout(() => { + app.listen({port: PORT}, () => { + console.log('🏭 using configuration from: ' + cred.CONFIGDB); + console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`) + }); +}, 10000); From c8d406e07d42e41abb048874afbcf2000fbe1849 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Wed, 24 Mar 2021 22:19:49 -0400 Subject: [PATCH 10/19] load tables differently --- src/TCRD.js | 4 +--- src/index.js | 4 ++-- src/models/databaseConfig.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/TCRD.js b/src/TCRD.js index 0c77ed4..fd06894 100644 --- a/src/TCRD.js +++ b/src/TCRD.js @@ -109,9 +109,7 @@ class TCRD extends SQLDataSource { super(config); const _this = this; this.tableInfo = new DatabaseConfig(this.db, config.connection.database, config.connection.configDB); - this.tableInfo.loadPromise.then(() => { - console.log('database tables loaded'); - }); + const root = { doid: 'DOID:4', name: 'disease', diff --git a/src/index.js b/src/index.js index 4dc80e8..331bebd 100644 --- a/src/index.js +++ b/src/index.js @@ -71,10 +71,10 @@ server.applyMiddleware({ }); const PORT = process.env.PORT || 4444; -setTimeout(() => { +tcrd.tableInfo.loadPromise.then(() => { app.listen({port: PORT}, () => { console.log('🏭 using configuration from: ' + cred.CONFIGDB); console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`) }); -}, 10000); +}); diff --git a/src/models/databaseConfig.ts b/src/models/databaseConfig.ts index b291636..d628d0e 100644 --- a/src/models/databaseConfig.ts +++ b/src/models/databaseConfig.ts @@ -210,7 +210,7 @@ export class DatabaseConfig { } loadModelMap() { - let query = this.database({...this.modelTable}).select('*').then((rows: any[]) => { + return this.database({...this.modelTable}).select('*').then((rows: any[]) => { rows.forEach(row => { this.modelList.set(row.name, new ModelInfo(row)); }); From 5cdc82b5157185add11526732eedc308b7d0a124 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Thu, 25 Mar 2021 00:07:54 -0400 Subject: [PATCH 11/19] dont allow GCP to shut down the server every 15 minutes --- app.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.yaml b/app.yaml index 82f5053..93a0dcc 100644 --- a/app.yaml +++ b/app.yaml @@ -1,2 +1,4 @@ instance_class: F4_1G runtime: nodejs10 +automatic_scaling: + min_instances: 1 From 2e461a86570a2e81be08e5e8c7d24bde710081ba Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Thu, 25 Mar 2021 01:31:24 -0400 Subject: [PATCH 12/19] revert port --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 331bebd..7d3c403 100644 --- a/src/index.js +++ b/src/index.js @@ -70,7 +70,7 @@ server.applyMiddleware({ path: '/graphql' }); -const PORT = process.env.PORT || 4444; +const PORT = process.env.PORT || 4000; tcrd.tableInfo.loadPromise.then(() => { app.listen({port: PORT}, () => { console.log('🏭 using configuration from: ' + cred.CONFIGDB); From 4e9b7c8cc7838c09b8ca12b6fed7b2fb9a217183 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Fri, 26 Mar 2021 15:47:57 -0400 Subject: [PATCH 13/19] batch support for ligands and diseases --- .../diseaseQueries/comprehensive.test.js | 46 ++++++++++++++++--- src/models/disease/diseaseList.ts | 8 ++++ src/models/ligand/ligandList.ts | 9 ++++ src/resolvers.js | 6 +++ 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/__tests__/diseaseQueries/comprehensive.test.js b/src/__tests__/diseaseQueries/comprehensive.test.js index e0b6d3a..fa7c764 100644 --- a/src/__tests__/diseaseQueries/comprehensive.test.js +++ b/src/__tests__/diseaseQueries/comprehensive.test.js @@ -11,9 +11,35 @@ beforeAll(() => { }); describe('all the queries should be consistent with each other', function () { + test('Disease batches are a thing too', () => { + const diseaseList = ["asthma", "benign ependymoma"]; + const fullList = new DiseaseList(tcrd); + const batchList = new DiseaseList(tcrd, { + batch: diseaseList + }); + + const batchListQuery = batchList.getListQuery(); + const batchCountQuery = batchList.getCountQuery(); + const fullTdlFacet = fullList.facetsToFetch.find(f => f.name == 'Target Development Level').getFacetQuery(); + const batchTdlFacet = batchList.facetsToFetch.find(f => f.name == 'Target Development Level').getFacetQuery(); + + return Promise.all([batchCountQuery, batchTdlFacet, fullTdlFacet, batchListQuery]).then(res => { + const batchCount = res[0][0].count; + expect(batchCount).toBe(diseaseList.length); + const batchLength = res[3].length; + expect(batchLength).toBe(diseaseList.length); + + const batchTdlCount = res[1].reduce((a, c) => a + c.value, 0); + const fullTdlCount = res[2].reduce((a, c) => a + c.value, 0); + expect(fullTdlCount).toBeGreaterThan(batchTdlCount); + expect(batchTdlCount).toBeGreaterThan(0); + }); + }); + + test('All diseases query', () => { - const fullList = new DiseaseList(tcrd, {top:20000}); - const filteredList = new DiseaseList(tcrd, {top:20000, filter: {facets: [{facet: "Data Source", values: ["CTD"]}]}}); + const fullList = new DiseaseList(tcrd); + const filteredList = new DiseaseList(tcrd, {filter: {facets: [{facet: "Data Source", values: ["CTD"]}]}}); const fullCountQuery = fullList.getCountQuery(); const fullListQuery = fullList.getListQuery(); @@ -59,8 +85,11 @@ describe('all the queries should be consistent with each other', function () { test('term search query', () => { - const fullList = new DiseaseList(tcrd, {top:20000, filter: {term:"cancer"}}); - const filteredList = new DiseaseList(tcrd, {top:20000, filter: {term:"cancer", facets: [{facet: "Data Source", values: ["CTD"]}]}}); + const fullList = new DiseaseList(tcrd, {top: 20000, filter: {term: "cancer"}}); + const filteredList = new DiseaseList(tcrd, { + top: 20000, + filter: {term: "cancer", facets: [{facet: "Data Source", values: ["CTD"]}]} + }); const fullCountQuery = fullList.getCountQuery(); const fullListQuery = fullList.getListQuery(); @@ -83,7 +112,7 @@ describe('all the queries should be consistent with each other', function () { expect(fullCount).toBe(fullListLength); const fullTypeCount = res[2].reduce((a, c) => a + c.value, 0); expect(fullTypeCount).toBeGreaterThanOrEqual(fullListLength); // only true for TDL because of multiple targets associated with these diseases - // ideally they'd be equal and add up exactly like in the target / ligand test facets + // ideally they'd be equal and add up exactly like in the target / ligand test facets const filteredCount = res[3][0].count; const filteredListLength = res[4].length; expect(filteredCount).toBe(filteredListLength); @@ -105,8 +134,11 @@ describe('all the queries should be consistent with each other', function () { }); test('associated target query', () => { - const fullList = new DiseaseList(tcrd, {top:20000, filter: {associatedTarget:"DRD2"}}); - const filteredList = new DiseaseList(tcrd, {top:20000, filter: {associatedTarget:"DRD2", facets: [{facet: "Data Source", values: ["CTD"]}]}}); + const fullList = new DiseaseList(tcrd, {top: 20000, filter: {associatedTarget: "DRD2"}}); + const filteredList = new DiseaseList(tcrd, { + top: 20000, + filter: {associatedTarget: "DRD2", facets: [{facet: "Data Source", values: ["CTD"]}]} + }); const fullCountQuery = fullList.getCountQuery(); const fullListQuery = fullList.getListQuery(); diff --git a/src/models/disease/diseaseList.ts b/src/models/disease/diseaseList.ts index d582cea..876bbc4 100644 --- a/src/models/disease/diseaseList.ts +++ b/src/models/disease/diseaseList.ts @@ -89,6 +89,14 @@ export class DiseaseList extends DataModelList { query.join(this.getAssociatedTargetQuery().as('assocTarget'), 'assocTarget.name', this.keyString()); } } + if (this.batch && this.batch.length > 0) { + query.join(this.getBatchQuery(this.batch).as('batchQuery'), 'batchQuery.disease_id', this.keyString()); + } + } + + getBatchQuery(batch: string[]){ + return this.database('ncats_disease').distinct({disease_id: 'id'}) + .whereIn('name', batch); } getTermQuery(){ diff --git a/src/models/ligand/ligandList.ts b/src/models/ligand/ligandList.ts index 495e7a9..51fe528 100644 --- a/src/models/ligand/ligandList.ts +++ b/src/models/ligand/ligandList.ts @@ -67,6 +67,15 @@ export class LigandList extends DataModelList { } else if (this.term.length > 0) { query.whereRaw(`match(name, ChEMBL, PubChem, \`Guide to Pharmacology\`, DrugCentral) against('${this.term}*')`); } + if (this.batch && this.batch.length > 0) { + query.join(this.getBatchQuery(this.batch).as('batchQuery'), 'batchQuery.ligand_id', this.keyString()); + } + } + + getBatchQuery(batch: string[]){ + return this.database('ncats_ligands').distinct({ligand_id: 'id'}) + .whereIn('identifier', batch) + .orWhereIn('name', batch); } tableNeedsInnerJoin(sqlTable: SqlTable) { diff --git a/src/resolvers.js b/src/resolvers.js index 32af343..33b00da 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -1379,6 +1379,7 @@ const resolvers = { facets: async (result, args, _) => filterResultFacets(result, args), diseases: async function (result, args, {dataSources}) { args.filter = result.filter; + args.batch = result.batch; return new DiseaseList(dataSources.tcrd, args).getListQuery() .then(diseases => { diseases.forEach(x => { @@ -1396,6 +1397,7 @@ const resolvers = { facets: async (result, args, _) => filterResultFacets(result, args), ligands: async function (result, args, {dataSources}) { args.filter = result.filter; + args.batch = result.batch; let ligandList = new LigandList(dataSources.tcrd, args); return ligandList.getListQuery().then( ligands => { @@ -1668,6 +1670,7 @@ function getTargetResult(args, dataSources) { } function getDiseaseResult(args, tcrd) { + args.batch = args.diseases; let diseaseList = new DiseaseList(tcrd, args); let queries = diseaseList.getFacetQueries(); queries.unshift(diseaseList.getCountQuery()); @@ -1691,6 +1694,7 @@ function getDiseaseResult(args, tcrd) { return { filter: args.filter, + batch: args.diseases, count: count, facets: facets }; @@ -1720,6 +1724,7 @@ function splitOnDelimiters(rowData) { } function getLigandResult(args, tcrd) { + args.batch = args.ligands; let ligandList = new LigandList(tcrd, args); const countQuery = ligandList.getCountQuery(); const facetQueries = ligandList.getFacetQueries(); @@ -1752,6 +1757,7 @@ function getLigandResult(args, tcrd) { } return { filter: args.filter, + batch: args.ligands, count: count, facets: facets }; From c6724b354c3c597632181667f5e6054813a102ee Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Fri, 26 Mar 2021 23:38:36 -0400 Subject: [PATCH 14/19] cap downloads as 250k rows. fix diseases with apostrophes --- src/models/disease/diseaseList.ts | 2 +- src/models/target/targetList.ts | 2 +- src/resolvers.js | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/models/disease/diseaseList.ts b/src/models/disease/diseaseList.ts index 876bbc4..16e3d82 100644 --- a/src/models/disease/diseaseList.ts +++ b/src/models/disease/diseaseList.ts @@ -18,7 +18,7 @@ export class DiseaseList extends DataModelList { static getDescendentsQuery(knex: any, diseaseName: string) { let finderQuery = knex("ncats_do") .min({lft: 'lft', rght: 'rght'}) - .whereRaw(`name = ?`, diseaseName); + .whereRaw(`name = "?"`, diseaseName); let query = knex({lst: 'ncats_do', finder: finderQuery}) .select('lst.name') .where('finder.lft', '<=', knex.raw('lst.lft')) diff --git a/src/models/target/targetList.ts b/src/models/target/targetList.ts index 7206429..151b70c 100644 --- a/src/models/target/targetList.ts +++ b/src/models/target/targetList.ts @@ -217,7 +217,7 @@ ON diseaseList.name = d.ncats_name`)); FROM ncats_do WHERE - name = '${this.associatedDisease}') AS finder + name = "${this.associatedDisease}") AS finder WHERE finder.lft <= lst.lft AND finder.rght + 0 >= lst.rght)`; diff --git a/src/resolvers.js b/src/resolvers.js index 33b00da..788f1dd 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -27,7 +27,6 @@ const resolvers = { return config.availableFieldMap.keys(); }, downloadLists: async function (config, args, {dataSources}) { - console.log(args); return Array.from(config.availableFieldMap.keys()).filter(listKey => { const pieces = listKey.split('-'); const reqModel = args.modelName ? args.modelName.toLowerCase() : ''; @@ -53,6 +52,11 @@ const resolvers = { download: async function (_, args, {dataSources}) { let listQuery; try { + if (args.top) { + args.top = Math.min(args.top, 250000); + }else{ + args.top = 250000; + } const listObj = DataModelListFactory.getListObject(args.model, dataSources.tcrd, args); listQuery = listObj.getListQuery(); } catch (e) { From c0fa8c9cd93234fcf4f05f21c5cd9d43751113a9 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 30 Mar 2021 00:23:36 -0400 Subject: [PATCH 15/19] handle tinx and pubmed downloads --- src/models/databaseTable.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/databaseTable.ts b/src/models/databaseTable.ts index 1f006bb..97981a6 100644 --- a/src/models/databaseTable.ts +++ b/src/models/databaseTable.ts @@ -69,7 +69,8 @@ export class DatabaseTable { ["ncats_disease-protein", ["disease", "ncats_d2da"]], ["disease-target", ["t2tc", "protein"]], ["protein-viral_protein", ["viral_ppi"]], - + ["protein-tinx_disease", ["tinx_importance"]], + ["protein-pubmed", ["protein2pubmed"]], ["protein-virus", ["viral_protein", "viral_ppi"]], ["protein-dto", ["p2dto"]], ["protein-panther_class", ["p2pc"]], From 74cb9f8a674b6b54d762c303239d920f752093de Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 30 Mar 2021 16:35:25 -0400 Subject: [PATCH 16/19] allow download of PPIs show warnings when the query limit must bee changed, like when all the pubmed abstracts are downloaded --- src/models/DataModelList.ts | 6 ++++++ src/models/queryDefinition.ts | 19 ++++++++++++++----- src/models/sqlTable.ts | 8 +++++--- src/models/target/targetList.ts | 14 ++++++++++++++ src/resolvers.js | 9 +++++---- src/schema.graphql | 1 + 6 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/models/DataModelList.ts b/src/models/DataModelList.ts index bc7fb81..4bd677b 100644 --- a/src/models/DataModelList.ts +++ b/src/models/DataModelList.ts @@ -20,6 +20,7 @@ export abstract class DataModelList implements IBuildable { facetFactory: FacetFactory; term: string = ""; fields: string[] = []; + warnings: string[] = []; rootTable: string; keyColumn: string; filteringFacets: FieldInfo[] = []; @@ -186,6 +187,7 @@ export abstract class DataModelList implements IBuildable { if (this.top) { query.limit(this.top); } + this.doSafetyCheck(query); // console.log(query.toString()); return query; } @@ -251,6 +253,10 @@ export abstract class DataModelList implements IBuildable { return dataFields; } + doSafetyCheck(query: any){ + // override to get this to do something + } + private pushOneDataField(field: string, context: string, dataFields: FieldInfo[]) { const fieldInfo = this.databaseConfig.getOneField(this.modelInfo.name, context, this.getAssociatedModel(), '', field); diff --git a/src/models/queryDefinition.ts b/src/models/queryDefinition.ts index 6a87055..19f5a99 100644 --- a/src/models/queryDefinition.ts +++ b/src/models/queryDefinition.ts @@ -38,12 +38,19 @@ export class QueryDefinition { return table.equals(reqData.table, reqData.where_clause); }); if (existingTable) { + this.updateTableAliasForColumn(reqData, existingTable.alias); existingTable.columns.push(reqData); return; } this.addRequestedDataToNewTable(reqData); } + updateTableAliasForColumn(reqData: FieldInfo, tableAlias: string){ + const re = new RegExp('\\b' + reqData.table + '\\b', 'ig'); + reqData.where_clause = reqData.where_clause?.replace(re, tableAlias); + reqData.select = reqData.select?.replace(re, tableAlias); + } + addRequestedDataToNewTable(reqData: FieldInfo) { let links: string[] = []; const tableCount = this.tables.filter(t => { @@ -51,11 +58,10 @@ export class QueryDefinition { }).length; let tableAlias = reqData.table; + let original_where_clause = reqData.where_clause; if(tableCount > 0){ tableAlias = reqData.table + tableCount; - const re = new RegExp('\\b' + reqData.table + '\\b', 'ig'); - reqData.where_clause = reqData.where_clause?.replace(re, tableAlias); - reqData.select = reqData.select?.replace(re, tableAlias); + this.updateTableAliasForColumn(reqData, tableAlias); } if (reqData.table != this.buildable.rootTable) { @@ -65,7 +71,8 @@ export class QueryDefinition { const newTable = new SqlTable(reqData.table, { alias: tableAlias, - joinConstraint: reqData.where_clause + joinConstraint: reqData.where_clause, + rawJoinConstraint: original_where_clause }, links); newTable.columns.push(reqData); this.tables.push(newTable); @@ -174,7 +181,9 @@ export class QueryDefinition { let linkInfo = buildableObj.databaseConfig.getLinkInformation(leftTable.tableName, dataTable.tableName); // @ts-ignore query[joinFunction]({[dataTable.alias]: dataTable.tableName}, function (this: any) { - this.on(`${leftTable.alias}.${linkInfo.fromCol}`, `=`, `${dataTable.alias}.${linkInfo.toCol}`); + if(leftTable.tableName !== dataTable.tableName || !dataTable.joinConstraint) { + this.on(`${leftTable.alias}.${linkInfo.fromCol}`, `=`, `${dataTable.alias}.${linkInfo.toCol}`); + } if (dataTable.joinConstraint) { this.andOn(buildableObj.database.raw(dataTable.joinConstraint)); } diff --git a/src/models/sqlTable.ts b/src/models/sqlTable.ts index 887cdbd..068e3c6 100644 --- a/src/models/sqlTable.ts +++ b/src/models/sqlTable.ts @@ -9,10 +9,11 @@ export class SqlTable { } joinConstraint: string; + rawJoinConstraint: string; columns: FieldInfo[] = []; linkingTables: string[] = []; - constructor(tableName: string, {alias = "", joinConstraint = ""} = {}, + constructor(tableName: string, {alias = "", joinConstraint = "", rawJoinConstraint = ""} = {}, linkingTables: string[] = []) { if (alias) { @@ -20,12 +21,13 @@ export class SqlTable { } this.tableName = tableName; this.joinConstraint = joinConstraint; + this.rawJoinConstraint = rawJoinConstraint; this.linkingTables = linkingTables; } equals(tableName: string, joinConstraint: string | undefined) { - if (!!this.joinConstraint || !!joinConstraint) { - return this.tableName === tableName && this.joinConstraint === joinConstraint; + if (this.rawJoinConstraint.length > 0 || !!joinConstraint) { + return this.tableName === tableName && this.rawJoinConstraint === joinConstraint; } return this.tableName === tableName; } diff --git a/src/models/target/targetList.ts b/src/models/target/targetList.ts index 151b70c..51276ff 100644 --- a/src/models/target/targetList.ts +++ b/src/models/target/targetList.ts @@ -224,4 +224,18 @@ ON diseaseList.name = d.ncats_name`)); } return ""; } + + + doSafetyCheck(query: any){ + if(this.fields.includes('Abstract')){ + if(this.top){ + query.limit(Math.min(this.top, 10000)); + } + else { + query.limit(10000); + } + this.warnings.push('Downloading abstracts is limited to 10,000 rows, due to size.') + } + // override to get this to do something + } } diff --git a/src/resolvers.js b/src/resolvers.js index 788f1dd..e4888d0 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -40,7 +40,7 @@ const resolvers = { if (pieces[2] == ''){ // normal fields should apply no matter the associated model return true; } - if (pieces[2] == reqAssocModel) { // otherwise, have to have the right associated model + if (pieces[2] == reqAssocModel) { // otherwise, have to have the right associated model, or be for a single entity return true; } return false; @@ -50,14 +50,14 @@ const resolvers = { Query: { download: async function (_, args, {dataSources}) { - let listQuery; + let listQuery, listObj; try { if (args.top) { args.top = Math.min(args.top, 250000); }else{ args.top = 250000; } - const listObj = DataModelListFactory.getListObject(args.model, dataSources.tcrd, args); + listObj = DataModelListFactory.getListObject(args.model, dataSources.tcrd, args); listQuery = listObj.getListQuery(); } catch (e) { return { @@ -68,7 +68,8 @@ const resolvers = { return { result: true, data: args.sqlOnly ? null : listQuery, - sql: listQuery.toString() + sql: listQuery.toString(), + warnings: listObj.warnings }; }, diff --git a/src/schema.graphql b/src/schema.graphql index bd9c802..f94e419 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -21,6 +21,7 @@ type DownloadResult { data: JSON errorDetails: JSON sql: String + warnings: [String] } From c036516524c2c3ea3576e67e2532d686683ce692 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 30 Mar 2021 18:04:35 -0400 Subject: [PATCH 17/19] fix bug with apostrophes in the search terms --- src/TCRD.js | 4 ++-- src/models/disease/diseaseList.ts | 2 +- src/models/ligand/ligandList.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/TCRD.js b/src/TCRD.js index fd06894..eefe68c 100644 --- a/src/TCRD.js +++ b/src/TCRD.js @@ -1787,9 +1787,9 @@ and c.target_id = ?`, [target.tcrdid])); category: this.db.raw("group_concat(`category` separator '|')"), reference_id: this.db.raw("group_concat(ifnull(`reference_id`,'') separator '|')") }) - .where("value", "like", `%${key}%`) + .where("value", "like", this.db.raw(`"%${key}%"`)) .groupBy("value") - .orderByRaw(`(case when value like '${key}%' then 1 else 2 end), length(value), count(*) desc`) + .orderByRaw(`(case when value like "${key}%" then 1 else 2 end), length(value), count(*) desc`) .limit(20); return q; } diff --git a/src/models/disease/diseaseList.ts b/src/models/disease/diseaseList.ts index 16e3d82..d319a76 100644 --- a/src/models/disease/diseaseList.ts +++ b/src/models/disease/diseaseList.ts @@ -102,7 +102,7 @@ export class DiseaseList extends DataModelList { getTermQuery(){ return this.database({ncats_disease: 'ncats_disease', ncats_d2da: 'ncats_d2da', disease: 'disease'}) .distinct({id: 'ncats_disease.id'}) - .whereRaw(`match(disease.ncats_name, disease.description, disease.drug_name) against('${this.term}*' in boolean mode)`) + .whereRaw(`match(disease.ncats_name, disease.description, disease.drug_name) against("${this.term}*" in boolean mode)`) .andWhere('ncats_disease.id', this.database.raw('ncats_d2da.ncats_disease_id')) .andWhere('ncats_d2da.disease_assoc_id', this.database.raw('disease.id')); } diff --git a/src/models/ligand/ligandList.ts b/src/models/ligand/ligandList.ts index 51fe528..7f807f6 100644 --- a/src/models/ligand/ligandList.ts +++ b/src/models/ligand/ligandList.ts @@ -65,7 +65,7 @@ export class LigandList extends DataModelList { query.join(associatedTargetQuery, 'assocTarget.identifier', 'ncats_ligands.identifier'); } } else if (this.term.length > 0) { - query.whereRaw(`match(name, ChEMBL, PubChem, \`Guide to Pharmacology\`, DrugCentral) against('${this.term}*')`); + query.whereRaw(`match(name, ChEMBL, PubChem, \`Guide to Pharmacology\`, DrugCentral) against("${this.term}*")`); } if (this.batch && this.batch.length > 0) { query.join(this.getBatchQuery(this.batch).as('batchQuery'), 'batchQuery.ligand_id', this.keyString()); From d611ccf11b091f599b8fe97a5b2135ef2a455e6c Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 30 Mar 2021 23:07:02 -0400 Subject: [PATCH 18/19] fix bug with apostrophes in the search terms --- src/TCRD.js | 2 +- src/models/FieldInfo.ts | 2 +- src/models/disease/diseaseList.ts | 4 ++-- src/models/ligand/ligandList.ts | 2 +- src/resolvers.js | 2 +- src/target_search.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/TCRD.js b/src/TCRD.js index eefe68c..fd4d72b 100644 --- a/src/TCRD.js +++ b/src/TCRD.js @@ -866,7 +866,7 @@ and f.itype = ?`, [filter.order])); for (var i in args.filter.facets) { let f = args.filter.facets[i]; if ('type' == f.facet) { - q.whereRaw(`ppitypes REGEXP '${f.values.join("|")}'`); + q.whereRaw(`ppitypes REGEXP "${f.values.join('|')}"`); } else { q = q.whereIn(f.facet, f.values); } diff --git a/src/models/FieldInfo.ts b/src/models/FieldInfo.ts index b2b710a..6f369a8 100644 --- a/src/models/FieldInfo.ts +++ b/src/models/FieldInfo.ts @@ -138,7 +138,7 @@ export class FieldInfo { } } else { if (this.valuesDelimited) { - query.where(this.parent.database.raw(`${this.select} REGEXP '${this.allowedValues.join('|')}'`)); + query.where(this.parent.database.raw(`${this.select} REGEXP "${this.allowedValues.join('|')}"`)); } else { query.whereIn(this.parent.database.raw(this.select), this.allowedValues); } diff --git a/src/models/disease/diseaseList.ts b/src/models/disease/diseaseList.ts index d319a76..b0b20f6 100644 --- a/src/models/disease/diseaseList.ts +++ b/src/models/disease/diseaseList.ts @@ -18,7 +18,7 @@ export class DiseaseList extends DataModelList { static getDescendentsQuery(knex: any, diseaseName: string) { let finderQuery = knex("ncats_do") .min({lft: 'lft', rght: 'rght'}) - .whereRaw(`name = "?"`, diseaseName); + .whereRaw(`name = ?`, diseaseName); let query = knex({lst: 'ncats_do', finder: finderQuery}) .select('lst.name') .where('finder.lft', '<=', knex.raw('lst.lft')) @@ -110,7 +110,7 @@ export class DiseaseList extends DataModelList { getAssociatedTargetQuery(): any { return this.database({ncats_disease: 'ncats_disease', ncats_d2da: 'ncats_d2da', disease: 'disease', protein: 'protein'}) .distinct({name: this.keyString()}).count('* as associationCount') - .whereRaw(this.database.raw(`match(uniprot,sym,stringid) against('${this.associatedTarget}' in boolean mode)`)) + .whereRaw(this.database.raw(`match(uniprot,sym,stringid) against("${this.associatedTarget}" in boolean mode)`)) .andWhere('ncats_disease.id', this.database.raw('ncats_d2da.ncats_disease_id')) .andWhere('ncats_d2da.disease_assoc_id', this.database.raw('disease.id')) .andWhere('disease.protein_id', this.database.raw(`protein.id`)) diff --git a/src/models/ligand/ligandList.ts b/src/models/ligand/ligandList.ts index 7f807f6..16edde5 100644 --- a/src/models/ligand/ligandList.ts +++ b/src/models/ligand/ligandList.ts @@ -58,7 +58,7 @@ export class LigandList extends DataModelList { protein: "protein" }) .distinct("ncats_ligands.identifier") - .whereRaw(this.database.raw(`match(uniprot,sym,stringid) against('${this.associatedTarget}' in boolean mode)`)) + .whereRaw(this.database.raw(`match(uniprot,sym,stringid) against("${this.associatedTarget}" in boolean mode)`)) .andWhere(this.database.raw(`ncats_ligands.id = ncats_ligand_activity.ncats_ligand_id`)) .andWhere(this.database.raw(`ncats_ligand_activity.target_id = t2tc.target_id`)) .andWhere(this.database.raw(`t2tc.protein_id = protein.id`)).as('assocTarget'); diff --git a/src/resolvers.js b/src/resolvers.js index e4888d0..864aa6e 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -1544,7 +1544,7 @@ const resolvers = { reference: 'reference', pubs: 'pubmed_ids' }) - .whereRaw(`ncats_ligands.identifier = '${ligand.ligid}'`) + .whereRaw(`ncats_ligands.identifier = "${ligand.ligid}"`) .andWhere(dataSources.tcrd.db.raw(`ncats_ligand_activity.ncats_ligand_id = ncats_ligands.id`)); if (dataSources.associatedTargetTCRDID) { query.andWhere(dataSources.tcrd.db.raw(`ncats_ligand_activity.target_id = ${dataSources.associatedTargetTCRDID}`)); diff --git a/src/target_search.js b/src/target_search.js index 62a3deb..61f9bfa 100644 --- a/src/target_search.js +++ b/src/target_search.js @@ -2,7 +2,7 @@ module.exports.getProteinListFromPPI = function(ppiTarget, confidence) { let proteinIDquery = this.db("protein") - .select("id").whereRaw(this.db.raw(`match(uniprot,sym,stringid) against('${ppiTarget}' in boolean mode)`)); + .select("id").whereRaw(this.db.raw(`match(uniprot,sym,stringid) against("${ppiTarget}" in boolean mode)`)); let ppiListQuery = this.db("ncats_ppi") .select(this.db.raw('distinct other_id as protein_id')) .whereIn('protein_id', proteinIDquery) From 99b55d7cc5fb676b2595263091635a03d9d536e4 Mon Sep 17 00:00:00 2001 From: Keith Kelleher Date: Tue, 30 Mar 2021 23:46:42 -0400 Subject: [PATCH 19/19] move to pharos_config_prod to ease the transition to the newly refactored backend --- src/db_credentials.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db_credentials.js b/src/db_credentials.js index 538c2db..37a3541 100644 --- a/src/db_credentials.js +++ b/src/db_credentials.js @@ -4,7 +4,7 @@ const cred = { DBHOST: 'tcrd.ncats.io', DBNAME: 'tcrd684', - CONFIGDB: 'pharos_config', + CONFIGDB: 'pharos_config_prod', USER: 'tcrd', PWORD: '' };