diff --git a/README.md b/README.md index 2ab1c09..6182bd4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Pharos graphql server ===================== -###To run +### To run Run `npm install` to get all dependencies, then diff --git a/src/index.js b/src/index.js index 5399448..bf3fd0c 100644 --- a/src/index.js +++ b/src/index.js @@ -56,7 +56,13 @@ const app = express(); app.get("/render", (req, res) => { const parsedUrl = url.parse(req.url); - res.redirect("https://tripod.nih.gov/servlet/renderServletv13?" + parsedUrl.query); + const pieces = parsedUrl.query.split('&'); + const paramMap = {}; + pieces.forEach(piece => { + const chunks = piece.split('='); + paramMap[chunks[0]] = chunks[1]; + }); + res.redirect(`https://tripod.nih.gov/idg/api/v1/render/${paramMap.structure}?size=${paramMap.size}`); }); server.applyMiddleware({ diff --git a/src/models/DataModelList.ts b/src/models/DataModelList.ts index c9d6f73..43aa248 100644 --- a/src/models/DataModelList.ts +++ b/src/models/DataModelList.ts @@ -1,7 +1,7 @@ import now from "performance-now"; import {FacetInfo} from "./FacetInfo"; import {FacetFactory} from "./FacetFactory"; -import {Config, ConfigKeys, QueryDefinition, SqlTable} from "./config"; +import {Config, ConfigKeys, QueryDefinition, RequestedData, SqlTable} from "./config"; import {DatabaseConfig} from "./databaseConfig"; // @ts-ignore import * as CONSTANTS from "../constants"; @@ -17,6 +17,7 @@ export abstract class DataModelList { facetFactory: FacetFactory; term: string = ""; + fields: string[] = []; rootTable: string; keyColumn: string; filteringFacets: FacetInfo[] = []; @@ -39,13 +40,13 @@ export abstract class DataModelList { get AllFacets(): string[] { const modelInfo = this.databaseConfig.modelList.get(this.rootTable); - const facetInfo = this.databaseConfig.fieldLists.get(`${modelInfo?.name} Facets - All`); + const facetInfo = this.databaseConfig.fieldLists.get(`${modelInfo?.name} Facet - All`); return facetInfo?.map(facet => facet.type) || []; } get DefaultFacets() { const modelInfo = this.databaseConfig.modelList.get(this.rootTable); - const facetInfo = this.databaseConfig.fieldLists.get(`${modelInfo?.name} Facets - Default`); + const facetInfo = this.databaseConfig.fieldLists.get(`${modelInfo?.name} Facet - Default`); return facetInfo?.sort((a,b) => a.order - b.order).map(a => a.type) || []; }; @@ -64,6 +65,9 @@ export abstract class DataModelList { if (json.top) { this.top = json.top; } + if (json.fields) { + this.fields = json.fields; + } } if (json && json.filter) { @@ -138,9 +142,15 @@ export abstract class DataModelList { getListQuery() { const that = this; - let dataFields = Config.GetDataFields(this.listQueryKey(), this.sortTable, this.sortColumn); + let dataFields: RequestedData[]; + 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(); + let rootTableObject = queryDefinition.getRootTable(); if (rootTableObject == undefined) { return; } @@ -148,6 +158,12 @@ export abstract class DataModelList { 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')); + } + } let query = this.database(queryDefinition.getTablesAsObjectArray(innerJoins)) .select(queryDefinition.getColumnList(this.database)); @@ -164,6 +180,11 @@ export abstract class DataModelList { 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){ @@ -189,12 +210,18 @@ export abstract class DataModelList { 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); - query.groupBy(this.keyString()); - + if(this.fields.length === 0) { + query.groupBy(this.keyString()); + } this.addSort(query, queryDefinition); if (this.skip) { query.offset(this.skip); @@ -202,7 +229,6 @@ export abstract class DataModelList { if (this.top) { query.limit(this.top); } - //console.log(query.toString()); this.captureQueryPerformance(query, "list count"); return query; } diff --git a/src/models/DataModelListFactory.ts b/src/models/DataModelListFactory.ts new file mode 100644 index 0000000..6135c31 --- /dev/null +++ b/src/models/DataModelListFactory.ts @@ -0,0 +1,19 @@ +import {DiseaseList} from "./disease/diseaseList"; +import {LigandList} from "./ligand/ligandList"; +import {TargetList} from "./target/targetList"; +import {DataModelList} from "./DataModelList"; + +export class DataModelListFactory { + static getListObject(modelName: string, tcrd: any, json: any): DataModelList { + if (modelName === "Diseases") { + return new DiseaseList(tcrd, json); + } + if (modelName === "Ligands") { + return new LigandList(tcrd, json); + } + if (modelName === "Targets") { + return new TargetList(tcrd, json); + } + throw new Error('Unknown Data Model: Expecting Diseases, Ligands, or Targets'); + } +} diff --git a/src/models/config.ts b/src/models/config.ts index 699469e..31fa66a 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -1,10 +1,31 @@ import {DatabaseTable} from "./databaseTable"; +import {DatabaseConfig} from "./databaseConfig"; /** * Class for gathering tables and columns to use for standard queries */ export class Config { - static GetDataFields(fieldKey: ConfigKeys, sortTable: string = "", sortColumn: string = ""): RequestedData[] { + 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 + * @param sortTable + * @param sortColumn + * @constructor + */ + static GetDataFieldsFromKey(fieldKey: ConfigKeys, sortTable: string = "", sortColumn: string = ""): RequestedData[] { const dataFields: RequestedData[] = []; switch (fieldKey) { case ConfigKeys.Target_List_Default: @@ -113,6 +134,7 @@ export class RequestedData { data: string = ""; alias?: string = ""; group_method?: string = ""; + where_clause?: string = ""; subQuery?: boolean = false; } @@ -151,34 +173,38 @@ export class QueryDefinition { } addRequestedDataToNewTable(reqData: RequestedData) { - const table = reqData.table; - const data = reqData.data; - const alias = reqData.alias; - const subq = reqData.subQuery; let links: string[] = []; const tableCount = this.tables.filter(t => { - return t.tableName == table; + return t.tableName == reqData.table; }).length; - if (DatabaseTable.sparseTables.includes(table)) { - this.tables.push(SqlTable.getSparseTableData(table, data, alias, table + tableCount)); + 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(table)) { - const typeColumn = DatabaseTable.typeTableColumns.get(table); - if (!typeColumn) throw new Error(`bad table configuration - ${table} has no type column configuration`); - const dataColumn = DatabaseTable.typeTableColumnMapping.get(table + "-" + data); - this.tables.push(SqlTable.getTypeTableData(table, data, typeColumn, (dataColumn || data), alias, table + tableCount)); + 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 (table != this.rootTable) { - links = DatabaseTable.getRequiredLinks(table, this.rootTable) || []; + if (reqData.table != this.rootTable) { + links = DatabaseTable.getRequiredLinks(reqData.table, this.rootTable) || []; } - const newTable = new SqlTable(table, {}, links, subq); - newTable.columns.push(new SqlColumns(reqData.data, reqData.alias, reqData.group_method)); + 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); } @@ -273,19 +299,22 @@ export class SqlTable { } } - static getTypeTableData(tableName: string, typeName: string, typeColumn: string, dataColumn: string, columnAlias?: string, tableAlias?: string) { + 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)); + typeTable.columns.push(new SqlColumns(dataColumn, columnAlias || dataColumn, group_method, where_clause)); return typeTable; } - static getSparseTableData(tableName: string, dataColumn: string, columnAlias?: string, tableAlias?: string) { + 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}); - sparseTable.columns.push(new SqlColumns(dataColumn, columnAlias || dataColumn)); + 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; } } @@ -294,15 +323,17 @@ 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 = "") { + 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; } diff --git a/src/models/databaseConfig.ts b/src/models/databaseConfig.ts index 5854703..ee36c53 100644 --- a/src/models/databaseConfig.ts +++ b/src/models/databaseConfig.ts @@ -62,7 +62,9 @@ export class DatabaseConfig { } fieldListColumns = { - listName: `fieldList.name`, + listType: `fieldList.type`, + listName: `fieldList.listName`, + field_id: `fieldList.field_id`, order: 'fieldList.order' }; fieldColumns = { @@ -105,21 +107,21 @@ export class DatabaseConfig { .whereRaw(this.linkFieldToModel); listQuery.then((rows: any[]) => { rows.forEach(row => { - if (this.fieldLists.has(row.listName)) { - const list = this.fieldLists.get(row.listName); + 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(row.listName, [row]); + this.fieldLists.set(listNameString, [row]); } }); }); const allFieldsQuery = this.database({...this.fieldTable, ...this.modelTable}) .select({...this.fieldColumns, ...this.modelColumns}) .whereRaw(this.linkFieldToModel); - // console.log(allFieldsQuery.toString()); allFieldsQuery.then((rows: any[]) => { rows.forEach(row => { - const keyName = `${row.modelName} Facets - All`; + const keyName = `${row.modelName} Facet - All`; if(row.isGoodForFacet) { if (this.fieldLists.has(keyName)) { const list = this.fieldLists.get(keyName); diff --git a/src/models/databaseTable.ts b/src/models/databaseTable.ts index c81fc33..d526690 100644 --- a/src/models/databaseTable.ts +++ b/src/models/databaseTable.ts @@ -117,14 +117,17 @@ export class DatabaseTable { static requiredLinks: Map = new Map( [ - ["protein-target", ["t2tc"]], ["protein-viral_protein", ["viral_ppi", "virus"]], ["protein-virus", ["viral_ppi", "viral_protein"]], ["protein-dto", ["p2dto"]], ["protein-panther_class", ["p2pc"]], ["protein-virus", ["viral_protein", "viral_ppi"]], ["protein-viral_protein", ["virus", "viral_ppi"]], - ["protein-ncats_ligands", ["ncats_ligand_activity", "target", "t2tc"]] + + // checked + ["protein-target", ["t2tc"]], + ["protein-ncats_ligands", ["t2tc", "target", "ncats_ligand_activity"]], + ["protein-ncats_ligand_activity", ["t2tc", "target"]] ]); static getRequiredLinks(table1: string, table2: string): string[] | undefined { diff --git a/src/models/disease/diseaseList.ts b/src/models/disease/diseaseList.ts index 8a627b7..e28705f 100644 --- a/src/models/disease/diseaseList.ts +++ b/src/models/disease/diseaseList.ts @@ -64,7 +64,7 @@ export class DiseaseList extends DataModelList { get DefaultFacetsWithTarget() { return this.databaseConfig.fieldLists - .get('Disease Facets - Associated Target')?.sort((a,b) => a.order - b.order) + .get('Disease Facet - Associated Target')?.sort((a,b) => a.order - b.order) .map(a => a.type) || []; }; defaultSortParameters(): {column: string; order: string}[] diff --git a/src/models/ligand/ligandList.ts b/src/models/ligand/ligandList.ts index 996ca33..2b7b758 100644 --- a/src/models/ligand/ligandList.ts +++ b/src/models/ligand/ligandList.ts @@ -19,10 +19,22 @@ export class LigandList extends DataModelList{ constructor(tcrd: any, json: any) { super(tcrd, "ncats_ligands" , "id", new LigandFacetFactory(), json); + let facetList: string[]; + if (this.associatedTarget) { + facetList = this.DefaultFacetsWithTarget; + } else { + facetList = this.DefaultFacets; + } this.facetsToFetch = FacetInfo.deduplicate( - this.facetsToFetch.concat(this.facetFactory.getFacetsFromList(this, this.DefaultFacets))); + 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) || []; + }; + defaultSortParameters(): {column: string; order: string}[] { return [{column: 'actcnt', order: 'desc'}]; diff --git a/src/models/target/targetList.ts b/src/models/target/targetList.ts index c724111..a0259c4 100644 --- a/src/models/target/targetList.ts +++ b/src/models/target/targetList.ts @@ -12,16 +12,19 @@ export class TargetList extends DataModelList { proteinListCached: boolean = false; 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'}]; } - else if(this.associatedTarget){ + if(this.associatedTarget){ return [{column:'p_int', order:'desc'},{column:'score', order:'desc'}]; } - else if(this.associatedDisease){ + if(this.associatedDisease){ return [{column: 'dtype', order:'desc'}]; } - else if(this.similarity.match.length > 0){ + if(this.similarity.match.length > 0){ return [{column: 'jaccard', order: 'desc'}]; } return [{column:'novelty', order:'desc'}]; @@ -85,7 +88,7 @@ export class TargetList extends DataModelList { query.join(subq.as("similarityQuery"), 'similarityQuery.protein_id', 'protein.id'); } } else { - if(!list || !this.associatedTarget) { + if(!list || !this.associatedTarget || (this.fields.length > 0)) { this.addProteinListConstraint(query, this.fetchProteinList()); } } @@ -242,13 +245,13 @@ ON diseaseList.name = d.ncats_name`)); get DefaultPPIFacets() { return this.databaseConfig.fieldLists - .get('Target Facets - Associated Target')?.sort((a,b) => a.order - b.order) + .get('Target Facet - Associated Target')?.sort((a,b) => a.order - b.order) .map(a => a.type) || []; }; get DefaultDiseaseFacets() { return this.databaseConfig.fieldLists - .get('Target Facets - Associated Disease')?.sort((a,b) => a.order - b.order) + .get('Target Facet - Associated Disease')?.sort((a,b) => a.order - b.order) .map(a => a.type) || []; }; } diff --git a/src/resolvers.js b/src/resolvers.js index a9eae5e..5b5f649 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -1,3 +1,4 @@ +const {DataModelListFactory} = require("./models/DataModelListFactory"); const {TargetDetails} = require("./models/target/targetDetails"); const {FacetDataType} = require("./models/FacetInfo"); const {LigandList} = require("./models/ligand/ligandList"); @@ -19,14 +20,36 @@ const resolvers = { PharosConfiguration: { lists: async function(config, args, {dataSources}){ - if(args.listName){ - return [args.listName]; + 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')); } }, Query: { + download: async function (_, args, {dataSources}){ + let listQuery; + try { + const listObj = DataModelListFactory.getListObject(args.model, dataSources.tcrd, args); + listQuery = listObj.getListQuery(); + } + catch(e){ + return { + result: false, + errorDetails: e.message + } + } + return { + result: true, + data: args.sqlOnly ? null : listQuery, + sql: listQuery.toString() + }; + }, + configuration: async function (_, args, {dataSources}){ return dataSources.tcrd.tableInfo; }, @@ -96,7 +119,7 @@ const resolvers = { }, targetFacets: async function (_, args, {dataSources}) { - return dataSources.tcrd.tableInfo.fieldLists.get('Target Facets - All').map(facet => facet.type); + return dataSources.tcrd.tableInfo.fieldLists.get('Target Facet - All').map(facet => facet.type); }, target: async function (_, args, {dataSources}) { diff --git a/src/schema.graphql b/src/schema.graphql index 0a8649a..4c6fe4b 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -14,6 +14,16 @@ enum FacetDataType { Numeric } +scalar JSON + +type DownloadResult { + result: Boolean + data: JSON + errorDetails: JSON + sql: String +} + + type Xref @cacheControl(maxAge: 604800) { source: String! name: String! @@ -648,7 +658,8 @@ type DataSourceCount @cacheControl(maxAge: 604800){ } type PharosConfiguration{ - lists (listName: String): [FieldList] + lists (listNames: [String]): [FieldList] + downloadLists (modelName: String): [FieldList] } type FieldList{ @@ -659,10 +670,10 @@ type FieldList{ type FieldDetails{ order: Int type: String - table: String - column: String + dataTable: String + dataColumn: String select: String - where_clause: String + whereClause: String null_table: String null_column: String null_count_column: String @@ -679,6 +690,8 @@ type FieldDetails{ type Query{ configuration: PharosConfiguration + download(model: String!, fields: [String!], sqlOnly: Boolean = true, skip: Int, top: Int, filter: IFilter, batch: [String]): DownloadResult + autocomplete(name: String): [SuggestionResults] dataSourceCounts: [DataSourceCount]