From 5cc9d042d08d1838c156e7badea49f361f36ecd7 Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Wed, 28 Sep 2022 18:15:42 -0700 Subject: [PATCH 01/34] create mega qedge --- src/edge_manager.js | 6 +- src/index.js | 21 +- src/mega_query_edge.js | 578 +++++++++++++++++++++++++++++++++++++++++ src/query_graph.js | 41 +-- 4 files changed, 604 insertions(+), 42 deletions(-) create mode 100644 src/mega_query_edge.js diff --git a/src/edge_manager.js b/src/edge_manager.js index aed6cfe8..1de3e1c6 100644 --- a/src/edge_manager.js +++ b/src/edge_manager.js @@ -325,7 +325,7 @@ module.exports = class QueryExecutionEdgeManager { this.logs = [...this.logs, ...qXEdge.logs]; //collect records combinedRecords = combinedRecords.concat(filteredRecords); - let connections = qXEdge.qEdge.subject.getConnections().concat(qXEdge.qEdge.object.getConnections()); + let connections = qXEdge.subject.getConnections().concat(qXEdge.object.getConnections()); connections = connections.filter(id => id !== qEdgeID); connections = new Set(connections); recordsByQEdgeID[qEdgeID] = { @@ -391,10 +391,10 @@ module.exports = class QueryExecutionEdgeManager { debug(`Updating neighbors...`); let currentQEdgeID = currentQXEdge.getID(); //get neighbors of this edges subject that are not this edge - let left_connections = currentQXEdge.qEdge.subject.getConnections(); + let left_connections = currentQXEdge.subject.getConnections(); left_connections = left_connections.filter((qEdgeID) => qEdgeID !== currentQEdgeID); //get neighbors of this edges object that are not this edge - let right_connections = currentQXEdge.qEdge.object.getConnections(); + let right_connections = currentQXEdge.object.getConnections(); right_connections = right_connections.filter((qEdgeID) => qEdgeID !== currentQEdgeID); debug(`(${left_connections})<--edge neighbors-->(${right_connections})`); if (left_connections.length) { diff --git a/src/index.js b/src/index.js index f891c28d..5f5a2bee 100644 --- a/src/index.js +++ b/src/index.js @@ -100,6 +100,7 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { if (err instanceof InvalidQueryGraphError || err instanceof id_resolver.SRIResolverFailiure) { throw err; } else { + console.log(err.stack); throw new InvalidQueryGraphError(); } } @@ -136,13 +137,13 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { let log_msg; if (currentQXEdge.reverse) { - log_msg = `qEdge ${currentQXEdge.qEdge.id} (reversed): ${currentQXEdge.qEdge.object.category} > ${ - currentQXEdge.qEdge.predicate ? `${currentQXEdge.qEdge.predicate} > ` : '' - }${currentQXEdge.qEdge.subject.category}`; + log_msg = `qEdge ${currentQXEdge.id} (reversed): ${currentQXEdge.object.category} > ${ + currentQXEdge.predicate ? `${currentQXEdge.predicate} > ` : '' + }${currentQXEdge.subject.category}`; } else { - log_msg = `qEdge ${currentQXEdge.qEdge.id}: ${currentQXEdge.qEdge.subject.category} > ${ - currentQXEdge.qEdge.predicate ? `${currentQXEdge.qEdge.predicate} > ` : '' - }${currentQXEdge.qEdge.object.category}`; + log_msg = `qEdge ${currentQXEdge.id}: ${currentQXEdge.subject.category} > ${ + currentQXEdge.predicate ? `${currentQXEdge.predicate} > ` : '' + }${currentQXEdge.object.category}`; } this.logs.push(new LogEntry('INFO', null, log_msg).getLog()); @@ -158,7 +159,7 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { } if (!metaXEdges.length) { - qEdgesMissingOps[currentQXEdge.qEdge.id] = currentQXEdge.reverse; + qEdgesMissingOps[currentQXEdge.id] = currentQXEdge.reverse; } // assume results so next edge may be reversed or not currentQXEdge.executed = true; @@ -400,10 +401,10 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { fail = 0, total = 0; let cached = this.logs.filter( - ({ data }) => data?.qEdgeID === currentQXEdge.qEdge.id && data?.type === 'cacheHit', + ({ data }) => data?.qEdgeID === currentQXEdge.id && data?.type === 'cacheHit', ).length; this.logs - .filter(({ data }) => data?.qEdgeID === currentQXEdge.qEdge.id && data?.type === 'query') + .filter(({ data }) => data?.qEdgeID === currentQXEdge.id && data?.type === 'query') .forEach(({ data }) => { !data.error ? success++ : fail++; total++; @@ -412,7 +413,7 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { new LogEntry( 'INFO', null, - `${currentQXEdge.qEdge.id} execution: ${total} queries (${success} success/${fail} fail) and (${cached}) cached qEdges return (${queryRecords.length}) records`, + `${currentQXEdge.id} execution: ${total} queries (${success} success/${fail} fail) and (${cached}) cached qEdges return (${queryRecords.length}) records`, {}, ).getLog(), ); diff --git a/src/mega_query_edge.js b/src/mega_query_edge.js new file mode 100644 index 00000000..084b5945 --- /dev/null +++ b/src/mega_query_edge.js @@ -0,0 +1,578 @@ +const helper = require('./helper'); +const debug = require('debug')('bte:biothings-explorer-trapi:MegaQEdge'); +const utils = require('./utils'); +const biolink = require('./biolink'); + +module.exports = class MegaQEdge { + /** + * + * @param {string} id - QEdge ID + * @param {object} info - QEdge info, e.g. subject, object, predicate + * @param {boolean} reverse - is QEdge reversed? + */ + constructor(id, info, reverse = false) { + this.id = id; + this.predicate = info.predicates; + this.subject = info.subject; + this.object = info.object; + this.expanded_predicates = []; + this.init(); + + this.reverse = reverse; + //object and subject aliases + this.input_equivalent_identifiers = {}; + this.output_equivalent_identifiers = {}; + //edge has been fully executed + this.executed = false; + //run initial checks + this.logs = []; + //this edges query response records + this.records = []; + debug(`(2) Created Edge` + + ` ${JSON.stringify(this.getID())} Reverse = ${this.reverse}`) + } + + init() { + this.expanded_predicates = this.getPredicate(); + } + + getID() { + return this.id; + } + + getHashedEdgeRepresentation() { + const toBeHashed = + this.subject.getCategories() + this.predicate + this.object.getCategories() + this.getInputCurie(); + return helper._generateHash(toBeHashed); + } + + expandPredicates(predicates) { + const reducer = (acc, cur) => [...acc, ...biolink.getDescendantPredicates(cur)]; + return Array.from(new Set(predicates.reduce(reducer, []))); + } + + getPredicate() { + if (this.predicate === undefined || this.predicate === null) { + return undefined; + } + const predicates = utils.toArray(this.predicate).map((item) => utils.removeBioLinkPrefix(item)); + const expandedPredicates = this.expandPredicates(predicates); + debug(`Expanded edges: ${expandedPredicates}`); + return expandedPredicates + .map((predicate) => { + return this.isReversed() === true ? biolink.reverse(predicate) : predicate; + }) + .filter((item) => !(typeof item === 'undefined')); + } + + getSubject() { + if (this.isReversed()) { + return this.object; + } + return this.subject; + } + + getObject() { + if (this.isReversed()) { + return this.subject; + } + return this.object; + } + + isReversed() { + return this.subject.getCurie() === undefined && this.object.getCurie() !== undefined; + } + + getInputCurie() { + let curie = this.subject.getCurie() || this.object.getCurie(); + if (Array.isArray(curie)) { + return curie; + } + return [curie]; + } + + getInputNode() { + return this.isReversed() ? this.object : this.subject; + } + + getOutputNode() { + return this.isReversed() ? this.subject : this.object; + } + + hasInputResolved() { + if (this.isReversed()) { + return this.object.hasEquivalentIDs(); + } + return this.subject.hasEquivalentIDs(); + } + + hasInput() { + if (this.isReversed()) { + return this.object.hasInput(); + } + return this.subject.hasInput(); + } + + chooseLowerEntityValue() { + //edge has both subject and object entity counts and must choose lower value + //to use in query. + debug(`(8) Choosing lower entity count in edge...`); + if (this.object.entity_count && this.subject.entity_count) { + if (this.object.entity_count == this.subject.entity_count) { + // //(#) ---> () + this.reverse = false; + this.object.holdCurie(); + debug(`(8) Sub - Obj were same but chose subject (${this.subject.entity_count})`); + } + else if (this.object.entity_count > this.subject.entity_count) { + //(#) ---> () + this.reverse = false; + //tell node to hold curie in a temp field + this.object.holdCurie(); + debug(`(8) Chose lower entity value in subject (${this.subject.entity_count})`); + } else { + //() <--- (#) + this.reverse = true; + //tell node to hold curie in a temp field + this.subject.holdCurie(); + debug(`(8) Chose lower entity value in object (${this.object.entity_count})`); + } + } else { + debug(`(8) Error: Edge must have both object and subject entity values.`); + } + } + + extractCuriesFromRecords(records, isReversed) { + //will give you all curies found by semantic type, each type will have + //a main ID and all of it's aliases + debug(`(7) Updating Entities in "${this.getID()}"`); + let typesToInclude = isReversed ? + this.subject.getCategories() : + this.object.getCategories(); + debug(`(7) Collecting Types: "${JSON.stringify(typesToInclude)}"`); + let all = {}; + records.forEach((record) => { + + record.subject.normalizedInfo.forEach((o) => { + //create semantic type if not included + let type = o._leafSemanticType; + if (typesToInclude.includes(type) || + typesToInclude.includes('NamedThing') || + typesToInclude.toString().includes(type)) { + if (!Object.hasOwnProperty.call(all, type)) { + all[type] = {}; + } + //get original and aliases + let original = record.subject.original; + //#1 prefer equivalent ids + if (Object.hasOwnProperty.call(o, '_dbIDs')) { + let original_aliases = new Set(); + for (const prefix in o._dbIDs) { + //check if array + if (Array.isArray(o._dbIDs[prefix])) { + o._dbIDs[prefix].forEach((single_alias) => { + if (single_alias) { + if (single_alias.includes(':')) { + //value already has prefix + original_aliases.add(single_alias); + } else { + //concat with prefix + original_aliases.add(prefix + ':' + single_alias); + } + } + }); + } else { + if (o._dbIDs[prefix].includes(':')) { + //value already has prefix + original_aliases.add(o._dbIDs[prefix]); + } else { + //concat with prefix + original_aliases.add(prefix + ':' + o._dbIDs[prefix]); + } + } + } + original_aliases = [...original_aliases]; + //check and add only unique + let was_found = false; + original_aliases.forEach((alias) => { + if (Object.hasOwnProperty.call(all[type], alias)) { + was_found = true; + } + }); + if (!was_found) { + all[type][original] = original_aliases; + } + } + //else #2 check curie + else if (Object.hasOwnProperty.call(o, 'curie')) { + if (Array.isArray( o.curie)) { + all[type][original] = o.curie; + } else { + all[type][original] = [o.curie]; + } + } + //#3 last resort check original + else { + all[type][original] = [original]; + } + } + }); + + record.object.normalizedInfo.forEach((o) => { + //create semantic type if not included + let type = o._leafSemanticType; + if (typesToInclude.includes(type) || + typesToInclude.includes('NamedThing') || + typesToInclude.toString().includes(type)) { + if (!Object.hasOwnProperty.call(all, type)) { + all[type] = {}; + } + //get original and aliases + let original = record.object.original; + + //#1 prefer equivalent ids + if (Object.hasOwnProperty.call(o, '_dbIDs')) { + let original_aliases = new Set(); + for (const prefix in o._dbIDs) { + //check if array + if (Array.isArray(o._dbIDs[prefix])) { + o._dbIDs[prefix].forEach((single_alias) => { + if (single_alias) { + if (single_alias.includes(':')) { + //value already has prefix + original_aliases.add(single_alias); + } else { + //concat with prefix + original_aliases.add(prefix + ':' + single_alias); + } + } + }); + } else { + if (o._dbIDs[prefix].includes(':')) { + //value already has prefix + original_aliases.add(o._dbIDs[prefix]); + } else { + //concat with prefix + original_aliases.add(prefix + ':' + o._dbIDs[prefix]); + } + } + } + original_aliases = [...original_aliases]; + //check and add only unique + let was_found = false; + original_aliases.forEach((alias) => { + if (Object.hasOwnProperty.call(all[type], alias)) { + was_found = true; + } + }); + if (!was_found) { + all[type][original] = original_aliases; + } + } + //else #2 check curie + else if (Object.hasOwnProperty.call(o, 'curie')) { + if (Array.isArray( o.curie)) { + all[type][original] = o.curie; + } else { + all[type][original] = [o.curie]; + } + } + //#3 last resort check original + else { + all[type][original] = [original]; + } + } + }); + + }); + // {Gene:{'id': ['alias']}} + debug(`Collected entity ids in records: ${JSON.stringify(Object.keys(all))}`); + return all; + } + + _combineCuries(curies) { + //combine all curies in case there are + //multiple categories in this node since + //they are separated by type + let combined = {}; + for (const type in curies) { + for (const original in curies[type]) { + combined[original] = curies[type][original]; + } + } + return combined; + } + + updateNodesCuries(records) { + //update node queried (1) ---> (update) + let curies_by_semantic_type = this.extractCuriesFromRecords(records, this.reverse); + let combined_curies = this._combineCuries(curies_by_semantic_type); + this.reverse ? + this.subject.updateCuries(combined_curies) : + this.object.updateCuries(combined_curies); + //update node used as input (1 [update]) ---> () + let curies_by_semantic_type_2 = this.extractCuriesFromRecords(records, !this.reverse); + let combined_curies_2 = this._combineCuries(curies_by_semantic_type_2); + !this.reverse ? + this.subject.updateCuries(combined_curies_2) : + this.object.updateCuries(combined_curies_2); + } + + applyNodeConstraints() { + debug(`(6) Applying Node Constraints to ${this.records.length} records.`); + let kept = []; + let save_kept = false; + let sub_constraints = this.subject.constraints; + if (sub_constraints && sub_constraints.length) { + let from = this.reverse ? 'object' : 'subject'; + debug(`Node (subject) constraints: ${JSON.stringify(sub_constraints)}`); + save_kept = true; + for (let i = 0; i < this.records.length; i++) { + const res = this.records[i]; + let keep = true; + //apply constraints + for (let x = 0; x < sub_constraints.length; x++) { + const constraint = sub_constraints[x]; + keep = this.meetsConstraint(constraint, res, from) + } + //pass or not + if (keep) { + kept.push(res); + } + } + } + + let obj_constraints = this.object.constraints; + if (obj_constraints && obj_constraints.length) { + let from = this.reverse ? 'subject' : 'object'; + debug(`Node (object) constraints: ${JSON.stringify(obj_constraints)}`); + save_kept = true; + for (let i = 0; i < this.records.length; i++) { + const res = this.records[i]; + let keep = true; + //apply constraints + for (let x = 0; x < obj_constraints.length; x++) { + const constraint = obj_constraints[x]; + keep = this.meetsConstraint(constraint, res, from) + } + //pass or not + if (keep) { + kept.push(res); + } + } + } + if (save_kept) { + //only override recordss if there was any filtering done. + this.records = kept; + debug(`(6) Reduced to (${this.records.length}) records.`); + } else { + debug(`(6) No constraints. Skipping...`); + } + } + + meetsConstraint(constraint, record, from) { + //list of attribute ids in node + let available_attributes = new Set(); + for (const key in record[from].attributes) { + available_attributes.add(key) + } + available_attributes = [...available_attributes]; + // debug(`ATTRS ${JSON.stringify(record[from].normalizedInfo[0]._leafSemanticType)}` + + // ` ${from} : ${JSON.stringify(available_attributes)}`); + //determine if node even contains right attributes + let filters_found = available_attributes.filter((attr) => attr == constraint.id); + if (!filters_found.length) { + //node doesn't have the attribute needed + return false; + } else { + //match attr by name, parse only attrs of interest + let node_attributes = {}; + filters_found.forEach((filter) => { + node_attributes[filter] = record[from].attributes[filter]; + }); + switch (constraint.operator) { + case "==": + for (const key in node_attributes) { + if (!isNaN(constraint.value)) { + if (Array.isArray(node_attributes[key])) { + if (node_attributes[key].includes(constraint.value) || + node_attributes[key].includes(constraint.value.toString())) { + return true; + } + } else { + if (node_attributes[key] == constraint.value || + node_attributes[key] == constraint.value.toString() || + node_attributes[key] == parseInt(constraint.value)) { + return true; + } + } + } else { + if (Array.isArray(node_attributes[key])) { + if (node_attributes[key].includes(constraint.value)) { + return true; + } + } else { + if (node_attributes[key] == constraint.value || + node_attributes[key] == constraint.value.toString() || + node_attributes[key] == parseInt(constraint.value)) { + return true; + } + } + } + } + return false; + case ">": + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) > parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) > parseInt(constraint.value)) { + return true; + } + } + } + return false; + case ">=": + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) >= parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) >= parseInt(constraint.value)) { + return true; + } + } + } + return false; + case "<": + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) > parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) < parseInt(constraint.value)) { + return true; + } + } + } + return false; + case "<=": + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) <= parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) <= parseInt(constraint.value)) { + return true; + } + } + } + return false; + default: + debug(`Node operator not handled ${constraint.operator}`); + return false; + }; + } + } + + storeRecords(records) { + debug(`(6) Storing records...`); + //store new records in current edge + this.records = records; + //will update records if any constraints are found + this.applyNodeConstraints(); + debug(`(7) Updating nodes based on edge records...`); + this.updateNodesCuries(records); + } + + getHashedEdgeRepresentation() { + const toBeHashed = + this.getSubject().getCategories() + this.getPredicate() + this.getObject().getCategories() + this.getInputCurie(); + return helper._generateHash(toBeHashed); + } + + expandPredicates(predicates) { + const reducer = (acc, cur) => [...acc, ...biolink.getDescendantPredicates(cur)]; + return Array.from(new Set(predicates.reduce(reducer, []))); + } + + getPredicate() { + if (this.predicate === undefined || this.predicate === null) { + return undefined; + } + const predicates = utils.toArray(this.predicate).map((item) => utils.removeBioLinkPrefix(item)); + const expandedPredicates = this.expandPredicates(predicates); + debug(`Expanded edges: ${expandedPredicates}`); + return expandedPredicates + .map((predicate) => { + return this.isReversed() === true ? biolink.reverse(predicate) : predicate; + }) + .filter((item) => !(typeof item === 'undefined')); + } + + getSubject() { + if (this.reverse) { + return this.object; + } + return this.subject; + } + + getObject() { + if (this.reverse) { + return this.subject; + } + return this.object; + } + + isReversed() { + return this.reverse; + } + + getInputCurie() { + let curie = this.subject.getCurie() || this.object.getCurie(); + if (Array.isArray(curie)) { + return curie; + } + return [curie]; + } + + getInputNode() { + return this.reverse ? this.object : this.subject; + } + + getOutputNode() { + return this.reverse ? this.subject : this.object; + } + + hasInputResolved() { + return !(Object.keys(this.input_equivalent_identifiers).length === 0); + } + + hasInput() { + if (this.reverse) { + return this.object.hasInput(); + } + return this.subject.hasInput(); + } + + getReversedPredicate(predicate) { + return predicate ? biolink.reverse(predicate) : undefined; + } +}; diff --git a/src/query_graph.js b/src/query_graph.js index 95729d9e..28acbb7b 100644 --- a/src/query_graph.js +++ b/src/query_graph.js @@ -2,6 +2,7 @@ const QEdge = require('./query_edge'); const InvalidQueryGraphError = require('./exceptions/invalid_query_graph_error'); const LogEntry = require('./log_entry'); const QueryExecutionEdge = require('./query_execution_edge'); +const MegaQEdge = require('./mega_query_edge'); const debug = require('debug')('bte:biothings-explorer-trapi:query_graph'); const QNode = require('./query_node'); const biolink = require('./biolink'); @@ -282,13 +283,18 @@ module.exports = class QueryGraphHandler { } /** - * @private + * */ - async _storeEdges() { + async calculateEdges() { + this._validate(this.queryGraph); + //populate edge and node info + debug(`(1) Creating edges for manager...`); if (this.nodes === undefined) { this.nodes = await this._storeNodes(); } + let edges = {}; + let edge_index = 0; for (let qEdgeID in this.queryGraph.edges) { let edge_info = { ...this.queryGraph.edges[qEdgeID], @@ -301,7 +307,10 @@ module.exports = class QueryGraphHandler { this.nodes[this.queryGraph.edges[qEdgeID].subject].updateConnection(qEdgeID); this.nodes[this.queryGraph.edges[qEdgeID].object].updateConnection(qEdgeID); - edges[qEdgeID] = new QEdge(qEdgeID, edge_info); + edges[edge_index] = [edge_info.object.curie ? + new MegaQEdge(qEdgeID, edge_info, true) : + new MegaQEdge(qEdgeID, edge_info, false)]; + edge_index++; } this.logs.push( new LogEntry('DEBUG', null, `BTE identified ${Object.keys(edges).length} qEdges from your query graph`).getLog(), @@ -309,30 +318,4 @@ module.exports = class QueryGraphHandler { return edges; } - /** - * - */ - async calculateEdges() { - this._validate(this.queryGraph); - //populate edge and node info - debug(`(1) Creating edges for manager...`); - if (this.edges === undefined) { - this.edges = await this._storeEdges(); - } - let edges = {}; - let edge_index = 0; - //create a smart query edge per edge in query - for (const qEdgeID in this.edges) { - edges[edge_index] = [ - // () ----> () - this.edges[qEdgeID].object.curie ? - new QueryExecutionEdge(this.edges[qEdgeID], true, undefined) : - new QueryExecutionEdge(this.edges[qEdgeID], false, undefined) - // reversed () <---- () - ]; - edge_index++; - } - return edges; - } - }; From d3bfab441a1019bd6e62e0944c8940962500d8e1 Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Fri, 30 Sep 2022 18:14:19 -0700 Subject: [PATCH 02/34] Work on freezing and unfreezing --- src/mega_query_edge.js | 80 +++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/src/mega_query_edge.js b/src/mega_query_edge.js index 084b5945..5fcab2be 100644 --- a/src/mega_query_edge.js +++ b/src/mega_query_edge.js @@ -2,6 +2,7 @@ const helper = require('./helper'); const debug = require('debug')('bte:biothings-explorer-trapi:MegaQEdge'); const utils = require('./utils'); const biolink = require('./biolink'); +const { Record } = require('@biothings-explorer/api-response-transform'); module.exports = class MegaQEdge { /** @@ -20,18 +21,41 @@ module.exports = class MegaQEdge { this.reverse = reverse; //object and subject aliases - this.input_equivalent_identifiers = {}; - this.output_equivalent_identifiers = {}; + this.input_equivalent_identifiers = info.input_equivalent_identifiers === undefined ? {} : info.input_equivalent_identifiers; + this.output_equivalent_identifiers = info.output_equivalent_identifiers === undefined ? {} : info.output_equivalent_identifiers; //edge has been fully executed - this.executed = false; + this.executed = info.executed === undefined ? false : info.executed; //run initial checks - this.logs = []; + this.logs = info.logs === undefined ? [] : info.logs; //this edges query response records this.records = []; debug(`(2) Created Edge` + ` ${JSON.stringify(this.getID())} Reverse = ${this.reverse}`) } + freeze() { + return { + id: this.id, + predicate: this.predicate, + expanded_predicates: this.expanded_predicates, + executed: this.executed, + reverse: this.reverse, + input_equivalent_identifiers: this.input_equivalent_identifiers, + logs: this.logs, + subject: this.subject, + object: this.object, + output_equivalent_identifiers: this.output_equivalent_identifiers, + predicate: this.predicate, + records: this.records.map(record => record.freeze()) + }; + } + + static unfreeze(json) { + var output = new MegaQEdge(json.id, json, json.reverse === undefined ? false : json.reverse); + output.records = json.records.map(recordJSON => new Record(recordJSON)); + return output; + } + init() { this.expanded_predicates = this.getPredicate(); } @@ -65,54 +89,6 @@ module.exports = class MegaQEdge { .filter((item) => !(typeof item === 'undefined')); } - getSubject() { - if (this.isReversed()) { - return this.object; - } - return this.subject; - } - - getObject() { - if (this.isReversed()) { - return this.subject; - } - return this.object; - } - - isReversed() { - return this.subject.getCurie() === undefined && this.object.getCurie() !== undefined; - } - - getInputCurie() { - let curie = this.subject.getCurie() || this.object.getCurie(); - if (Array.isArray(curie)) { - return curie; - } - return [curie]; - } - - getInputNode() { - return this.isReversed() ? this.object : this.subject; - } - - getOutputNode() { - return this.isReversed() ? this.subject : this.object; - } - - hasInputResolved() { - if (this.isReversed()) { - return this.object.hasEquivalentIDs(); - } - return this.subject.hasEquivalentIDs(); - } - - hasInput() { - if (this.isReversed()) { - return this.object.hasInput(); - } - return this.subject.hasInput(); - } - chooseLowerEntityValue() { //edge has both subject and object entity counts and must choose lower value //to use in query. From 6db8ada0399238f8c7a4878ece6443fce49399dc Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Mon, 3 Oct 2022 17:43:54 -0700 Subject: [PATCH 03/34] Add freezing for QNode --- src/mega_query_edge.js | 11 ++++++----- src/query_node.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/mega_query_edge.js b/src/mega_query_edge.js index 5fcab2be..939c5559 100644 --- a/src/mega_query_edge.js +++ b/src/mega_query_edge.js @@ -3,6 +3,7 @@ const debug = require('debug')('bte:biothings-explorer-trapi:MegaQEdge'); const utils = require('./utils'); const biolink = require('./biolink'); const { Record } = require('@biothings-explorer/api-response-transform'); +const QNode = require('./query_node'); module.exports = class MegaQEdge { /** @@ -14,8 +15,8 @@ module.exports = class MegaQEdge { constructor(id, info, reverse = false) { this.id = id; this.predicate = info.predicates; - this.subject = info.subject; - this.object = info.object; + this.subject = info.frozen === true ? QNode.unfreeze(info.subject) : info.subject; + this.object = info.frozen === true ? QNode.unfreeze(info.object) : info.object; this.expanded_predicates = []; this.init(); @@ -42,8 +43,8 @@ module.exports = class MegaQEdge { reverse: this.reverse, input_equivalent_identifiers: this.input_equivalent_identifiers, logs: this.logs, - subject: this.subject, - object: this.object, + subject: this.subject.freeze(), + object: this.object.freeze(), output_equivalent_identifiers: this.output_equivalent_identifiers, predicate: this.predicate, records: this.records.map(record => record.freeze()) @@ -51,7 +52,7 @@ module.exports = class MegaQEdge { } static unfreeze(json) { - var output = new MegaQEdge(json.id, json, json.reverse === undefined ? false : json.reverse); + var output = new MegaQEdge(json.id, { ...json, frozen: true }, json.reverse === undefined ? false : json.reverse); output.records = json.records.map(recordJSON => new Record(recordJSON)); return output; } diff --git a/src/query_node.js b/src/query_node.js index f0c8cccf..aa18da7e 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -18,22 +18,43 @@ module.exports = class QNode { //is_set this.is_set = info.is_set; //mainID : its equivalent ids - this.expanded_curie = {}; + this.expanded_curie = info.expanded_curie !== undefined ? info.expanded_curie : {}; this.entity_count = info.ids ? info.ids.length : 0; debug(`(1) Node "${this.id}" has (${this.entity_count}) entities at start.`); //when choosing a lower entity count a node with higher count // might be told to store its curies temporarily - this.held_curie = []; - this.held_expanded = {}; + this.held_curie = info.held_curie !== undefined ? info.held_curie : []; + this.held_expanded = info.held_expanded !== undefined ? info.held_expanded : {}; //node constraints this.constraints = info.constraints; //list of edge ids that are connected to this node - this.connected_to = new Set(); + this.connected_to = info.connected_to !== undefined ? new Set(info.connected_to) : new Set(); //object-ify array of initial curies - this.expandCurie(); + if (info.expanded_curie === undefined) this.expandCurie(); this.validateConstraints(); } + freeze() { + return { + category: this.category, + connected_to: Array.from(this.connected_to), + constraints: this.constraints, + curie: this.curie, + entity_count: this.entity_count, + equivalentIDs: this.equivalentIDs, + expanded_curie: this.expanded_curie, + held_curie: this.held_curie, + held_expanded: this.held_expanded, + id: this.id, + is_set: this.is_set + } + } + + static unfreeze(json) { + var node = new QNode(json.id, json); + return node; + } + isSet() { //query node specified as set return this.is_set ? true : false; From 06b39bd3c6c4d89771468d6f13d439102d61f5ca Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Mon, 3 Oct 2022 17:47:51 -0700 Subject: [PATCH 04/34] Migrate mega q edge to q edge --- src/mega_query_edge.js | 555 ------------------------------------ src/query_edge.js | 488 +++++++++++++++++++++++++++++-- src/query_execution_edge.js | 501 -------------------------------- src/query_graph.js | 6 +- 4 files changed, 473 insertions(+), 1077 deletions(-) delete mode 100644 src/mega_query_edge.js delete mode 100644 src/query_execution_edge.js diff --git a/src/mega_query_edge.js b/src/mega_query_edge.js deleted file mode 100644 index 939c5559..00000000 --- a/src/mega_query_edge.js +++ /dev/null @@ -1,555 +0,0 @@ -const helper = require('./helper'); -const debug = require('debug')('bte:biothings-explorer-trapi:MegaQEdge'); -const utils = require('./utils'); -const biolink = require('./biolink'); -const { Record } = require('@biothings-explorer/api-response-transform'); -const QNode = require('./query_node'); - -module.exports = class MegaQEdge { - /** - * - * @param {string} id - QEdge ID - * @param {object} info - QEdge info, e.g. subject, object, predicate - * @param {boolean} reverse - is QEdge reversed? - */ - constructor(id, info, reverse = false) { - this.id = id; - this.predicate = info.predicates; - this.subject = info.frozen === true ? QNode.unfreeze(info.subject) : info.subject; - this.object = info.frozen === true ? QNode.unfreeze(info.object) : info.object; - this.expanded_predicates = []; - this.init(); - - this.reverse = reverse; - //object and subject aliases - this.input_equivalent_identifiers = info.input_equivalent_identifiers === undefined ? {} : info.input_equivalent_identifiers; - this.output_equivalent_identifiers = info.output_equivalent_identifiers === undefined ? {} : info.output_equivalent_identifiers; - //edge has been fully executed - this.executed = info.executed === undefined ? false : info.executed; - //run initial checks - this.logs = info.logs === undefined ? [] : info.logs; - //this edges query response records - this.records = []; - debug(`(2) Created Edge` + - ` ${JSON.stringify(this.getID())} Reverse = ${this.reverse}`) - } - - freeze() { - return { - id: this.id, - predicate: this.predicate, - expanded_predicates: this.expanded_predicates, - executed: this.executed, - reverse: this.reverse, - input_equivalent_identifiers: this.input_equivalent_identifiers, - logs: this.logs, - subject: this.subject.freeze(), - object: this.object.freeze(), - output_equivalent_identifiers: this.output_equivalent_identifiers, - predicate: this.predicate, - records: this.records.map(record => record.freeze()) - }; - } - - static unfreeze(json) { - var output = new MegaQEdge(json.id, { ...json, frozen: true }, json.reverse === undefined ? false : json.reverse); - output.records = json.records.map(recordJSON => new Record(recordJSON)); - return output; - } - - init() { - this.expanded_predicates = this.getPredicate(); - } - - getID() { - return this.id; - } - - getHashedEdgeRepresentation() { - const toBeHashed = - this.subject.getCategories() + this.predicate + this.object.getCategories() + this.getInputCurie(); - return helper._generateHash(toBeHashed); - } - - expandPredicates(predicates) { - const reducer = (acc, cur) => [...acc, ...biolink.getDescendantPredicates(cur)]; - return Array.from(new Set(predicates.reduce(reducer, []))); - } - - getPredicate() { - if (this.predicate === undefined || this.predicate === null) { - return undefined; - } - const predicates = utils.toArray(this.predicate).map((item) => utils.removeBioLinkPrefix(item)); - const expandedPredicates = this.expandPredicates(predicates); - debug(`Expanded edges: ${expandedPredicates}`); - return expandedPredicates - .map((predicate) => { - return this.isReversed() === true ? biolink.reverse(predicate) : predicate; - }) - .filter((item) => !(typeof item === 'undefined')); - } - - chooseLowerEntityValue() { - //edge has both subject and object entity counts and must choose lower value - //to use in query. - debug(`(8) Choosing lower entity count in edge...`); - if (this.object.entity_count && this.subject.entity_count) { - if (this.object.entity_count == this.subject.entity_count) { - // //(#) ---> () - this.reverse = false; - this.object.holdCurie(); - debug(`(8) Sub - Obj were same but chose subject (${this.subject.entity_count})`); - } - else if (this.object.entity_count > this.subject.entity_count) { - //(#) ---> () - this.reverse = false; - //tell node to hold curie in a temp field - this.object.holdCurie(); - debug(`(8) Chose lower entity value in subject (${this.subject.entity_count})`); - } else { - //() <--- (#) - this.reverse = true; - //tell node to hold curie in a temp field - this.subject.holdCurie(); - debug(`(8) Chose lower entity value in object (${this.object.entity_count})`); - } - } else { - debug(`(8) Error: Edge must have both object and subject entity values.`); - } - } - - extractCuriesFromRecords(records, isReversed) { - //will give you all curies found by semantic type, each type will have - //a main ID and all of it's aliases - debug(`(7) Updating Entities in "${this.getID()}"`); - let typesToInclude = isReversed ? - this.subject.getCategories() : - this.object.getCategories(); - debug(`(7) Collecting Types: "${JSON.stringify(typesToInclude)}"`); - let all = {}; - records.forEach((record) => { - - record.subject.normalizedInfo.forEach((o) => { - //create semantic type if not included - let type = o._leafSemanticType; - if (typesToInclude.includes(type) || - typesToInclude.includes('NamedThing') || - typesToInclude.toString().includes(type)) { - if (!Object.hasOwnProperty.call(all, type)) { - all[type] = {}; - } - //get original and aliases - let original = record.subject.original; - //#1 prefer equivalent ids - if (Object.hasOwnProperty.call(o, '_dbIDs')) { - let original_aliases = new Set(); - for (const prefix in o._dbIDs) { - //check if array - if (Array.isArray(o._dbIDs[prefix])) { - o._dbIDs[prefix].forEach((single_alias) => { - if (single_alias) { - if (single_alias.includes(':')) { - //value already has prefix - original_aliases.add(single_alias); - } else { - //concat with prefix - original_aliases.add(prefix + ':' + single_alias); - } - } - }); - } else { - if (o._dbIDs[prefix].includes(':')) { - //value already has prefix - original_aliases.add(o._dbIDs[prefix]); - } else { - //concat with prefix - original_aliases.add(prefix + ':' + o._dbIDs[prefix]); - } - } - } - original_aliases = [...original_aliases]; - //check and add only unique - let was_found = false; - original_aliases.forEach((alias) => { - if (Object.hasOwnProperty.call(all[type], alias)) { - was_found = true; - } - }); - if (!was_found) { - all[type][original] = original_aliases; - } - } - //else #2 check curie - else if (Object.hasOwnProperty.call(o, 'curie')) { - if (Array.isArray( o.curie)) { - all[type][original] = o.curie; - } else { - all[type][original] = [o.curie]; - } - } - //#3 last resort check original - else { - all[type][original] = [original]; - } - } - }); - - record.object.normalizedInfo.forEach((o) => { - //create semantic type if not included - let type = o._leafSemanticType; - if (typesToInclude.includes(type) || - typesToInclude.includes('NamedThing') || - typesToInclude.toString().includes(type)) { - if (!Object.hasOwnProperty.call(all, type)) { - all[type] = {}; - } - //get original and aliases - let original = record.object.original; - - //#1 prefer equivalent ids - if (Object.hasOwnProperty.call(o, '_dbIDs')) { - let original_aliases = new Set(); - for (const prefix in o._dbIDs) { - //check if array - if (Array.isArray(o._dbIDs[prefix])) { - o._dbIDs[prefix].forEach((single_alias) => { - if (single_alias) { - if (single_alias.includes(':')) { - //value already has prefix - original_aliases.add(single_alias); - } else { - //concat with prefix - original_aliases.add(prefix + ':' + single_alias); - } - } - }); - } else { - if (o._dbIDs[prefix].includes(':')) { - //value already has prefix - original_aliases.add(o._dbIDs[prefix]); - } else { - //concat with prefix - original_aliases.add(prefix + ':' + o._dbIDs[prefix]); - } - } - } - original_aliases = [...original_aliases]; - //check and add only unique - let was_found = false; - original_aliases.forEach((alias) => { - if (Object.hasOwnProperty.call(all[type], alias)) { - was_found = true; - } - }); - if (!was_found) { - all[type][original] = original_aliases; - } - } - //else #2 check curie - else if (Object.hasOwnProperty.call(o, 'curie')) { - if (Array.isArray( o.curie)) { - all[type][original] = o.curie; - } else { - all[type][original] = [o.curie]; - } - } - //#3 last resort check original - else { - all[type][original] = [original]; - } - } - }); - - }); - // {Gene:{'id': ['alias']}} - debug(`Collected entity ids in records: ${JSON.stringify(Object.keys(all))}`); - return all; - } - - _combineCuries(curies) { - //combine all curies in case there are - //multiple categories in this node since - //they are separated by type - let combined = {}; - for (const type in curies) { - for (const original in curies[type]) { - combined[original] = curies[type][original]; - } - } - return combined; - } - - updateNodesCuries(records) { - //update node queried (1) ---> (update) - let curies_by_semantic_type = this.extractCuriesFromRecords(records, this.reverse); - let combined_curies = this._combineCuries(curies_by_semantic_type); - this.reverse ? - this.subject.updateCuries(combined_curies) : - this.object.updateCuries(combined_curies); - //update node used as input (1 [update]) ---> () - let curies_by_semantic_type_2 = this.extractCuriesFromRecords(records, !this.reverse); - let combined_curies_2 = this._combineCuries(curies_by_semantic_type_2); - !this.reverse ? - this.subject.updateCuries(combined_curies_2) : - this.object.updateCuries(combined_curies_2); - } - - applyNodeConstraints() { - debug(`(6) Applying Node Constraints to ${this.records.length} records.`); - let kept = []; - let save_kept = false; - let sub_constraints = this.subject.constraints; - if (sub_constraints && sub_constraints.length) { - let from = this.reverse ? 'object' : 'subject'; - debug(`Node (subject) constraints: ${JSON.stringify(sub_constraints)}`); - save_kept = true; - for (let i = 0; i < this.records.length; i++) { - const res = this.records[i]; - let keep = true; - //apply constraints - for (let x = 0; x < sub_constraints.length; x++) { - const constraint = sub_constraints[x]; - keep = this.meetsConstraint(constraint, res, from) - } - //pass or not - if (keep) { - kept.push(res); - } - } - } - - let obj_constraints = this.object.constraints; - if (obj_constraints && obj_constraints.length) { - let from = this.reverse ? 'subject' : 'object'; - debug(`Node (object) constraints: ${JSON.stringify(obj_constraints)}`); - save_kept = true; - for (let i = 0; i < this.records.length; i++) { - const res = this.records[i]; - let keep = true; - //apply constraints - for (let x = 0; x < obj_constraints.length; x++) { - const constraint = obj_constraints[x]; - keep = this.meetsConstraint(constraint, res, from) - } - //pass or not - if (keep) { - kept.push(res); - } - } - } - if (save_kept) { - //only override recordss if there was any filtering done. - this.records = kept; - debug(`(6) Reduced to (${this.records.length}) records.`); - } else { - debug(`(6) No constraints. Skipping...`); - } - } - - meetsConstraint(constraint, record, from) { - //list of attribute ids in node - let available_attributes = new Set(); - for (const key in record[from].attributes) { - available_attributes.add(key) - } - available_attributes = [...available_attributes]; - // debug(`ATTRS ${JSON.stringify(record[from].normalizedInfo[0]._leafSemanticType)}` + - // ` ${from} : ${JSON.stringify(available_attributes)}`); - //determine if node even contains right attributes - let filters_found = available_attributes.filter((attr) => attr == constraint.id); - if (!filters_found.length) { - //node doesn't have the attribute needed - return false; - } else { - //match attr by name, parse only attrs of interest - let node_attributes = {}; - filters_found.forEach((filter) => { - node_attributes[filter] = record[from].attributes[filter]; - }); - switch (constraint.operator) { - case "==": - for (const key in node_attributes) { - if (!isNaN(constraint.value)) { - if (Array.isArray(node_attributes[key])) { - if (node_attributes[key].includes(constraint.value) || - node_attributes[key].includes(constraint.value.toString())) { - return true; - } - } else { - if (node_attributes[key] == constraint.value || - node_attributes[key] == constraint.value.toString() || - node_attributes[key] == parseInt(constraint.value)) { - return true; - } - } - } else { - if (Array.isArray(node_attributes[key])) { - if (node_attributes[key].includes(constraint.value)) { - return true; - } - } else { - if (node_attributes[key] == constraint.value || - node_attributes[key] == constraint.value.toString() || - node_attributes[key] == parseInt(constraint.value)) { - return true; - } - } - } - } - return false; - case ">": - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) > parseInt(constraint.value)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) > parseInt(constraint.value)) { - return true; - } - } - } - return false; - case ">=": - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) >= parseInt(constraint.value)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) >= parseInt(constraint.value)) { - return true; - } - } - } - return false; - case "<": - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) > parseInt(constraint.value)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) < parseInt(constraint.value)) { - return true; - } - } - } - return false; - case "<=": - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) <= parseInt(constraint.value)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) <= parseInt(constraint.value)) { - return true; - } - } - } - return false; - default: - debug(`Node operator not handled ${constraint.operator}`); - return false; - }; - } - } - - storeRecords(records) { - debug(`(6) Storing records...`); - //store new records in current edge - this.records = records; - //will update records if any constraints are found - this.applyNodeConstraints(); - debug(`(7) Updating nodes based on edge records...`); - this.updateNodesCuries(records); - } - - getHashedEdgeRepresentation() { - const toBeHashed = - this.getSubject().getCategories() + this.getPredicate() + this.getObject().getCategories() + this.getInputCurie(); - return helper._generateHash(toBeHashed); - } - - expandPredicates(predicates) { - const reducer = (acc, cur) => [...acc, ...biolink.getDescendantPredicates(cur)]; - return Array.from(new Set(predicates.reduce(reducer, []))); - } - - getPredicate() { - if (this.predicate === undefined || this.predicate === null) { - return undefined; - } - const predicates = utils.toArray(this.predicate).map((item) => utils.removeBioLinkPrefix(item)); - const expandedPredicates = this.expandPredicates(predicates); - debug(`Expanded edges: ${expandedPredicates}`); - return expandedPredicates - .map((predicate) => { - return this.isReversed() === true ? biolink.reverse(predicate) : predicate; - }) - .filter((item) => !(typeof item === 'undefined')); - } - - getSubject() { - if (this.reverse) { - return this.object; - } - return this.subject; - } - - getObject() { - if (this.reverse) { - return this.subject; - } - return this.object; - } - - isReversed() { - return this.reverse; - } - - getInputCurie() { - let curie = this.subject.getCurie() || this.object.getCurie(); - if (Array.isArray(curie)) { - return curie; - } - return [curie]; - } - - getInputNode() { - return this.reverse ? this.object : this.subject; - } - - getOutputNode() { - return this.reverse ? this.subject : this.object; - } - - hasInputResolved() { - return !(Object.keys(this.input_equivalent_identifiers).length === 0); - } - - hasInput() { - if (this.reverse) { - return this.object.hasInput(); - } - return this.subject.hasInput(); - } - - getReversedPredicate(predicate) { - return predicate ? biolink.reverse(predicate) : undefined; - } -}; diff --git a/src/query_edge.js b/src/query_edge.js index 65325710..283f1f1b 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -1,21 +1,60 @@ const helper = require('./helper'); -const debug = require('debug')('bte:biothings-explorer-trapi:QEdge'); +const debug = require('debug')('bte:biothings-explorer-trapi:MegaQEdge'); const utils = require('./utils'); -const reverse = require('./biolink'); +const biolink = require('./biolink'); +const { Record } = require('@biothings-explorer/api-response-transform'); +const QNode = require('./query_node'); module.exports = class QEdge { /** * * @param {string} id - QEdge ID * @param {object} info - QEdge info, e.g. subject, object, predicate + * @param {boolean} reverse - is QEdge reversed? */ - constructor(id, info) { + constructor(id, info, reverse = false) { this.id = id; this.predicate = info.predicates; - this.subject = info.subject; - this.object = info.object; + this.subject = info.frozen === true ? QNode.unfreeze(info.subject) : info.subject; + this.object = info.frozen === true ? QNode.unfreeze(info.object) : info.object; this.expanded_predicates = []; this.init(); + + this.reverse = reverse; + //object and subject aliases + this.input_equivalent_identifiers = info.input_equivalent_identifiers === undefined ? {} : info.input_equivalent_identifiers; + this.output_equivalent_identifiers = info.output_equivalent_identifiers === undefined ? {} : info.output_equivalent_identifiers; + //edge has been fully executed + this.executed = info.executed === undefined ? false : info.executed; + //run initial checks + this.logs = info.logs === undefined ? [] : info.logs; + //this edges query response records + this.records = []; + debug(`(2) Created Edge` + + ` ${JSON.stringify(this.getID())} Reverse = ${this.reverse}`) + } + + freeze() { + return { + id: this.id, + predicate: this.predicate, + expanded_predicates: this.expanded_predicates, + executed: this.executed, + reverse: this.reverse, + input_equivalent_identifiers: this.input_equivalent_identifiers, + logs: this.logs, + subject: this.subject.freeze(), + object: this.object.freeze(), + output_equivalent_identifiers: this.output_equivalent_identifiers, + predicate: this.predicate, + records: this.records.map(record => record.freeze()) + }; + } + + static unfreeze(json) { + var output = new MegaQEdge(json.id, { ...json, frozen: true }, json.reverse === undefined ? false : json.reverse); + output.records = json.records.map(recordJSON => new Record(recordJSON)); + return output; } init() { @@ -33,7 +72,421 @@ module.exports = class QEdge { } expandPredicates(predicates) { - const reducer = (acc, cur) => [...acc, ...reverse.getDescendantPredicates(cur)]; + const reducer = (acc, cur) => [...acc, ...biolink.getDescendantPredicates(cur)]; + return Array.from(new Set(predicates.reduce(reducer, []))); + } + + getPredicate() { + if (this.predicate === undefined || this.predicate === null) { + return undefined; + } + const predicates = utils.toArray(this.predicate).map((item) => utils.removeBioLinkPrefix(item)); + const expandedPredicates = this.expandPredicates(predicates); + debug(`Expanded edges: ${expandedPredicates}`); + return expandedPredicates + .map((predicate) => { + return this.isReversed() === true ? biolink.reverse(predicate) : predicate; + }) + .filter((item) => !(typeof item === 'undefined')); + } + + chooseLowerEntityValue() { + //edge has both subject and object entity counts and must choose lower value + //to use in query. + debug(`(8) Choosing lower entity count in edge...`); + if (this.object.entity_count && this.subject.entity_count) { + if (this.object.entity_count == this.subject.entity_count) { + // //(#) ---> () + this.reverse = false; + this.object.holdCurie(); + debug(`(8) Sub - Obj were same but chose subject (${this.subject.entity_count})`); + } + else if (this.object.entity_count > this.subject.entity_count) { + //(#) ---> () + this.reverse = false; + //tell node to hold curie in a temp field + this.object.holdCurie(); + debug(`(8) Chose lower entity value in subject (${this.subject.entity_count})`); + } else { + //() <--- (#) + this.reverse = true; + //tell node to hold curie in a temp field + this.subject.holdCurie(); + debug(`(8) Chose lower entity value in object (${this.object.entity_count})`); + } + } else { + debug(`(8) Error: Edge must have both object and subject entity values.`); + } + } + + extractCuriesFromRecords(records, isReversed) { + //will give you all curies found by semantic type, each type will have + //a main ID and all of it's aliases + debug(`(7) Updating Entities in "${this.getID()}"`); + let typesToInclude = isReversed ? + this.subject.getCategories() : + this.object.getCategories(); + debug(`(7) Collecting Types: "${JSON.stringify(typesToInclude)}"`); + let all = {}; + records.forEach((record) => { + + record.subject.normalizedInfo.forEach((o) => { + //create semantic type if not included + let type = o._leafSemanticType; + if (typesToInclude.includes(type) || + typesToInclude.includes('NamedThing') || + typesToInclude.toString().includes(type)) { + if (!Object.hasOwnProperty.call(all, type)) { + all[type] = {}; + } + //get original and aliases + let original = record.subject.original; + //#1 prefer equivalent ids + if (Object.hasOwnProperty.call(o, '_dbIDs')) { + let original_aliases = new Set(); + for (const prefix in o._dbIDs) { + //check if array + if (Array.isArray(o._dbIDs[prefix])) { + o._dbIDs[prefix].forEach((single_alias) => { + if (single_alias) { + if (single_alias.includes(':')) { + //value already has prefix + original_aliases.add(single_alias); + } else { + //concat with prefix + original_aliases.add(prefix + ':' + single_alias); + } + } + }); + } else { + if (o._dbIDs[prefix].includes(':')) { + //value already has prefix + original_aliases.add(o._dbIDs[prefix]); + } else { + //concat with prefix + original_aliases.add(prefix + ':' + o._dbIDs[prefix]); + } + } + } + original_aliases = [...original_aliases]; + //check and add only unique + let was_found = false; + original_aliases.forEach((alias) => { + if (Object.hasOwnProperty.call(all[type], alias)) { + was_found = true; + } + }); + if (!was_found) { + all[type][original] = original_aliases; + } + } + //else #2 check curie + else if (Object.hasOwnProperty.call(o, 'curie')) { + if (Array.isArray( o.curie)) { + all[type][original] = o.curie; + } else { + all[type][original] = [o.curie]; + } + } + //#3 last resort check original + else { + all[type][original] = [original]; + } + } + }); + + record.object.normalizedInfo.forEach((o) => { + //create semantic type if not included + let type = o._leafSemanticType; + if (typesToInclude.includes(type) || + typesToInclude.includes('NamedThing') || + typesToInclude.toString().includes(type)) { + if (!Object.hasOwnProperty.call(all, type)) { + all[type] = {}; + } + //get original and aliases + let original = record.object.original; + + //#1 prefer equivalent ids + if (Object.hasOwnProperty.call(o, '_dbIDs')) { + let original_aliases = new Set(); + for (const prefix in o._dbIDs) { + //check if array + if (Array.isArray(o._dbIDs[prefix])) { + o._dbIDs[prefix].forEach((single_alias) => { + if (single_alias) { + if (single_alias.includes(':')) { + //value already has prefix + original_aliases.add(single_alias); + } else { + //concat with prefix + original_aliases.add(prefix + ':' + single_alias); + } + } + }); + } else { + if (o._dbIDs[prefix].includes(':')) { + //value already has prefix + original_aliases.add(o._dbIDs[prefix]); + } else { + //concat with prefix + original_aliases.add(prefix + ':' + o._dbIDs[prefix]); + } + } + } + original_aliases = [...original_aliases]; + //check and add only unique + let was_found = false; + original_aliases.forEach((alias) => { + if (Object.hasOwnProperty.call(all[type], alias)) { + was_found = true; + } + }); + if (!was_found) { + all[type][original] = original_aliases; + } + } + //else #2 check curie + else if (Object.hasOwnProperty.call(o, 'curie')) { + if (Array.isArray( o.curie)) { + all[type][original] = o.curie; + } else { + all[type][original] = [o.curie]; + } + } + //#3 last resort check original + else { + all[type][original] = [original]; + } + } + }); + + }); + // {Gene:{'id': ['alias']}} + debug(`Collected entity ids in records: ${JSON.stringify(Object.keys(all))}`); + return all; + } + + _combineCuries(curies) { + //combine all curies in case there are + //multiple categories in this node since + //they are separated by type + let combined = {}; + for (const type in curies) { + for (const original in curies[type]) { + combined[original] = curies[type][original]; + } + } + return combined; + } + + updateNodesCuries(records) { + //update node queried (1) ---> (update) + let curies_by_semantic_type = this.extractCuriesFromRecords(records, this.reverse); + let combined_curies = this._combineCuries(curies_by_semantic_type); + this.reverse ? + this.subject.updateCuries(combined_curies) : + this.object.updateCuries(combined_curies); + //update node used as input (1 [update]) ---> () + let curies_by_semantic_type_2 = this.extractCuriesFromRecords(records, !this.reverse); + let combined_curies_2 = this._combineCuries(curies_by_semantic_type_2); + !this.reverse ? + this.subject.updateCuries(combined_curies_2) : + this.object.updateCuries(combined_curies_2); + } + + applyNodeConstraints() { + debug(`(6) Applying Node Constraints to ${this.records.length} records.`); + let kept = []; + let save_kept = false; + let sub_constraints = this.subject.constraints; + if (sub_constraints && sub_constraints.length) { + let from = this.reverse ? 'object' : 'subject'; + debug(`Node (subject) constraints: ${JSON.stringify(sub_constraints)}`); + save_kept = true; + for (let i = 0; i < this.records.length; i++) { + const res = this.records[i]; + let keep = true; + //apply constraints + for (let x = 0; x < sub_constraints.length; x++) { + const constraint = sub_constraints[x]; + keep = this.meetsConstraint(constraint, res, from) + } + //pass or not + if (keep) { + kept.push(res); + } + } + } + + let obj_constraints = this.object.constraints; + if (obj_constraints && obj_constraints.length) { + let from = this.reverse ? 'subject' : 'object'; + debug(`Node (object) constraints: ${JSON.stringify(obj_constraints)}`); + save_kept = true; + for (let i = 0; i < this.records.length; i++) { + const res = this.records[i]; + let keep = true; + //apply constraints + for (let x = 0; x < obj_constraints.length; x++) { + const constraint = obj_constraints[x]; + keep = this.meetsConstraint(constraint, res, from) + } + //pass or not + if (keep) { + kept.push(res); + } + } + } + if (save_kept) { + //only override recordss if there was any filtering done. + this.records = kept; + debug(`(6) Reduced to (${this.records.length}) records.`); + } else { + debug(`(6) No constraints. Skipping...`); + } + } + + meetsConstraint(constraint, record, from) { + //list of attribute ids in node + let available_attributes = new Set(); + for (const key in record[from].attributes) { + available_attributes.add(key) + } + available_attributes = [...available_attributes]; + // debug(`ATTRS ${JSON.stringify(record[from].normalizedInfo[0]._leafSemanticType)}` + + // ` ${from} : ${JSON.stringify(available_attributes)}`); + //determine if node even contains right attributes + let filters_found = available_attributes.filter((attr) => attr == constraint.id); + if (!filters_found.length) { + //node doesn't have the attribute needed + return false; + } else { + //match attr by name, parse only attrs of interest + let node_attributes = {}; + filters_found.forEach((filter) => { + node_attributes[filter] = record[from].attributes[filter]; + }); + switch (constraint.operator) { + case "==": + for (const key in node_attributes) { + if (!isNaN(constraint.value)) { + if (Array.isArray(node_attributes[key])) { + if (node_attributes[key].includes(constraint.value) || + node_attributes[key].includes(constraint.value.toString())) { + return true; + } + } else { + if (node_attributes[key] == constraint.value || + node_attributes[key] == constraint.value.toString() || + node_attributes[key] == parseInt(constraint.value)) { + return true; + } + } + } else { + if (Array.isArray(node_attributes[key])) { + if (node_attributes[key].includes(constraint.value)) { + return true; + } + } else { + if (node_attributes[key] == constraint.value || + node_attributes[key] == constraint.value.toString() || + node_attributes[key] == parseInt(constraint.value)) { + return true; + } + } + } + } + return false; + case ">": + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) > parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) > parseInt(constraint.value)) { + return true; + } + } + } + return false; + case ">=": + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) >= parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) >= parseInt(constraint.value)) { + return true; + } + } + } + return false; + case "<": + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) > parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) < parseInt(constraint.value)) { + return true; + } + } + } + return false; + case "<=": + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) <= parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) <= parseInt(constraint.value)) { + return true; + } + } + } + return false; + default: + debug(`Node operator not handled ${constraint.operator}`); + return false; + }; + } + } + + storeRecords(records) { + debug(`(6) Storing records...`); + //store new records in current edge + this.records = records; + //will update records if any constraints are found + this.applyNodeConstraints(); + debug(`(7) Updating nodes based on edge records...`); + this.updateNodesCuries(records); + } + + getHashedEdgeRepresentation() { + const toBeHashed = + this.getSubject().getCategories() + this.getPredicate() + this.getObject().getCategories() + this.getInputCurie(); + return helper._generateHash(toBeHashed); + } + + expandPredicates(predicates) { + const reducer = (acc, cur) => [...acc, ...biolink.getDescendantPredicates(cur)]; return Array.from(new Set(predicates.reduce(reducer, []))); } @@ -46,27 +499,27 @@ module.exports = class QEdge { debug(`Expanded edges: ${expandedPredicates}`); return expandedPredicates .map((predicate) => { - return this.isReversed() === true ? reverse.reverse(predicate) : predicate; + return this.isReversed() === true ? biolink.reverse(predicate) : predicate; }) .filter((item) => !(typeof item === 'undefined')); } getSubject() { - if (this.isReversed()) { + if (this.reverse) { return this.object; } return this.subject; } getObject() { - if (this.isReversed()) { + if (this.reverse) { return this.subject; } return this.object; } isReversed() { - return this.subject.getCurie() === undefined && this.object.getCurie() !== undefined; + return this.reverse; } getInputCurie() { @@ -78,24 +531,25 @@ module.exports = class QEdge { } getInputNode() { - return this.isReversed() ? this.object : this.subject; + return this.reverse ? this.object : this.subject; } getOutputNode() { - return this.isReversed() ? this.subject : this.object; + return this.reverse ? this.subject : this.object; } hasInputResolved() { - if (this.isReversed()) { - return this.object.hasEquivalentIDs(); - } - return this.subject.hasEquivalentIDs(); + return !(Object.keys(this.input_equivalent_identifiers).length === 0); } hasInput() { - if (this.isReversed()) { + if (this.reverse) { return this.object.hasInput(); } return this.subject.hasInput(); } + + getReversedPredicate(predicate) { + return predicate ? biolink.reverse(predicate) : undefined; + } }; diff --git a/src/query_execution_edge.js b/src/query_execution_edge.js deleted file mode 100644 index 3b74010f..00000000 --- a/src/query_execution_edge.js +++ /dev/null @@ -1,501 +0,0 @@ -const helper = require('./helper'); -const debug = require('debug')('bte:biothings-explorer-trapi:QueryExecutionEdge'); -const utils = require('./utils'); -const biolink = require('./biolink'); - -module.exports = class QueryExecutionEdge { - /** - * - * @param {string} id - QEdge ID - * @param {object} info - QEdge info, e.g. subject, object, predicate - */ - constructor(qEdge, reverse = false, prev_edge = undefined) { - this.qEdge = qEdge; - //nodes that make up this edge - // this.connecting_nodes = []; - this.reverse = reverse; - this.prev_edge = prev_edge; - //object and subject aliases - this.input_equivalent_identifiers = {}; - this.output_equivalent_identifiers = {}; - //instances of query_node - //this.object/subject are instances of QNode - this.object = qEdge.object; - this.subject = qEdge.subject; - //edge has been fully executed - this.executed = false; - //run initial checks - this.logs = []; - //this edges query response records - this.records = []; - debug(`(2) Created Edge` + - ` ${JSON.stringify(this.qEdge.getID())} Reverse = ${this.reverse}`) - } - - chooseLowerEntityValue() { - //edge has both subject and object entity counts and must choose lower value - //to use in query. - debug(`(8) Choosing lower entity count in edge...`); - if (this.qEdge.object.entity_count && this.qEdge.subject.entity_count) { - if (this.qEdge.object.entity_count == this.qEdge.subject.entity_count) { - // //(#) ---> () - this.reverse = false; - this.qEdge.object.holdCurie(); - debug(`(8) Sub - Obj were same but chose subject (${this.qEdge.subject.entity_count})`); - } - else if (this.qEdge.object.entity_count > this.qEdge.subject.entity_count) { - //(#) ---> () - this.reverse = false; - //tell node to hold curie in a temp field - this.qEdge.object.holdCurie(); - debug(`(8) Chose lower entity value in subject (${this.qEdge.subject.entity_count})`); - } else { - //() <--- (#) - this.reverse = true; - //tell node to hold curie in a temp field - this.qEdge.subject.holdCurie(); - debug(`(8) Chose lower entity value in object (${this.qEdge.object.entity_count})`); - } - } else { - debug(`(8) Error: Edge must have both object and subject entity values.`); - } - } - - extractCuriesFromRecords(records, isReversed) { - //will give you all curies found by semantic type, each type will have - //a main ID and all of it's aliases - debug(`(7) Updating Entities in "${this.qEdge.getID()}"`); - let typesToInclude = isReversed ? - this.qEdge.subject.getCategories() : - this.qEdge.object.getCategories(); - debug(`(7) Collecting Types: "${JSON.stringify(typesToInclude)}"`); - let all = {}; - records.forEach((record) => { - - record.subject.normalizedInfo.forEach((o) => { - //create semantic type if not included - let type = o._leafSemanticType; - if (typesToInclude.includes(type) || - typesToInclude.includes('NamedThing') || - typesToInclude.toString().includes(type)) { - if (!Object.hasOwnProperty.call(all, type)) { - all[type] = {}; - } - //get original and aliases - let original = record.subject.original; - //#1 prefer equivalent ids - if (Object.hasOwnProperty.call(o, '_dbIDs')) { - let original_aliases = new Set(); - for (const prefix in o._dbIDs) { - //check if array - if (Array.isArray(o._dbIDs[prefix])) { - o._dbIDs[prefix].forEach((single_alias) => { - if (single_alias) { - if (single_alias.includes(':')) { - //value already has prefix - original_aliases.add(single_alias); - } else { - //concat with prefix - original_aliases.add(prefix + ':' + single_alias); - } - } - }); - } else { - if (o._dbIDs[prefix].includes(':')) { - //value already has prefix - original_aliases.add(o._dbIDs[prefix]); - } else { - //concat with prefix - original_aliases.add(prefix + ':' + o._dbIDs[prefix]); - } - } - } - original_aliases = [...original_aliases]; - //check and add only unique - let was_found = false; - original_aliases.forEach((alias) => { - if (Object.hasOwnProperty.call(all[type], alias)) { - was_found = true; - } - }); - if (!was_found) { - all[type][original] = original_aliases; - } - } - //else #2 check curie - else if (Object.hasOwnProperty.call(o, 'curie')) { - if (Array.isArray( o.curie)) { - all[type][original] = o.curie; - } else { - all[type][original] = [o.curie]; - } - } - //#3 last resort check original - else { - all[type][original] = [original]; - } - } - }); - - record.object.normalizedInfo.forEach((o) => { - //create semantic type if not included - let type = o._leafSemanticType; - if (typesToInclude.includes(type) || - typesToInclude.includes('NamedThing') || - typesToInclude.toString().includes(type)) { - if (!Object.hasOwnProperty.call(all, type)) { - all[type] = {}; - } - //get original and aliases - let original = record.object.original; - - //#1 prefer equivalent ids - if (Object.hasOwnProperty.call(o, '_dbIDs')) { - let original_aliases = new Set(); - for (const prefix in o._dbIDs) { - //check if array - if (Array.isArray(o._dbIDs[prefix])) { - o._dbIDs[prefix].forEach((single_alias) => { - if (single_alias) { - if (single_alias.includes(':')) { - //value already has prefix - original_aliases.add(single_alias); - } else { - //concat with prefix - original_aliases.add(prefix + ':' + single_alias); - } - } - }); - } else { - if (o._dbIDs[prefix].includes(':')) { - //value already has prefix - original_aliases.add(o._dbIDs[prefix]); - } else { - //concat with prefix - original_aliases.add(prefix + ':' + o._dbIDs[prefix]); - } - } - } - original_aliases = [...original_aliases]; - //check and add only unique - let was_found = false; - original_aliases.forEach((alias) => { - if (Object.hasOwnProperty.call(all[type], alias)) { - was_found = true; - } - }); - if (!was_found) { - all[type][original] = original_aliases; - } - } - //else #2 check curie - else if (Object.hasOwnProperty.call(o, 'curie')) { - if (Array.isArray( o.curie)) { - all[type][original] = o.curie; - } else { - all[type][original] = [o.curie]; - } - } - //#3 last resort check original - else { - all[type][original] = [original]; - } - } - }); - - }); - // {Gene:{'id': ['alias']}} - debug(`Collected entity ids in records: ${JSON.stringify(Object.keys(all))}`); - return all; - } - - _combineCuries(curies) { - //combine all curies in case there are - //multiple categories in this node since - //they are separated by type - let combined = {}; - for (const type in curies) { - for (const original in curies[type]) { - combined[original] = curies[type][original]; - } - } - return combined; - } - - updateNodesCuries(records) { - //update node queried (1) ---> (update) - let curies_by_semantic_type = this.extractCuriesFromRecords(records, this.reverse); - let combined_curies = this._combineCuries(curies_by_semantic_type); - this.reverse ? - this.qEdge.subject.updateCuries(combined_curies) : - this.qEdge.object.updateCuries(combined_curies); - //update node used as input (1 [update]) ---> () - let curies_by_semantic_type_2 = this.extractCuriesFromRecords(records, !this.reverse); - let combined_curies_2 = this._combineCuries(curies_by_semantic_type_2); - !this.reverse ? - this.qEdge.subject.updateCuries(combined_curies_2) : - this.qEdge.object.updateCuries(combined_curies_2); - } - - applyNodeConstraints() { - debug(`(6) Applying Node Constraints to ${this.records.length} records.`); - let kept = []; - let save_kept = false; - let sub_constraints = this.subject.constraints; - if (sub_constraints && sub_constraints.length) { - let from = this.reverse ? 'object' : 'subject'; - debug(`Node (subject) constraints: ${JSON.stringify(sub_constraints)}`); - save_kept = true; - for (let i = 0; i < this.records.length; i++) { - const res = this.records[i]; - let keep = true; - //apply constraints - for (let x = 0; x < sub_constraints.length; x++) { - const constraint = sub_constraints[x]; - keep = this.meetsConstraint(constraint, res, from) - } - //pass or not - if (keep) { - kept.push(res); - } - } - } - - let obj_constraints = this.object.constraints; - if (obj_constraints && obj_constraints.length) { - let from = this.reverse ? 'subject' : 'object'; - debug(`Node (object) constraints: ${JSON.stringify(obj_constraints)}`); - save_kept = true; - for (let i = 0; i < this.records.length; i++) { - const res = this.records[i]; - let keep = true; - //apply constraints - for (let x = 0; x < obj_constraints.length; x++) { - const constraint = obj_constraints[x]; - keep = this.meetsConstraint(constraint, res, from) - } - //pass or not - if (keep) { - kept.push(res); - } - } - } - if (save_kept) { - //only override recordss if there was any filtering done. - this.records = kept; - debug(`(6) Reduced to (${this.records.length}) records.`); - } else { - debug(`(6) No constraints. Skipping...`); - } - } - - meetsConstraint(constraint, record, from) { - //list of attribute ids in node - let available_attributes = new Set(); - for (const key in record[from].attributes) { - available_attributes.add(key) - } - available_attributes = [...available_attributes]; - // debug(`ATTRS ${JSON.stringify(record[from].normalizedInfo[0]._leafSemanticType)}` + - // ` ${from} : ${JSON.stringify(available_attributes)}`); - //determine if node even contains right attributes - let filters_found = available_attributes.filter((attr) => attr == constraint.id); - if (!filters_found.length) { - //node doesn't have the attribute needed - return false; - } else { - //match attr by name, parse only attrs of interest - let node_attributes = {}; - filters_found.forEach((filter) => { - node_attributes[filter] = record[from].attributes[filter]; - }); - switch (constraint.operator) { - case "==": - for (const key in node_attributes) { - if (!isNaN(constraint.value)) { - if (Array.isArray(node_attributes[key])) { - if (node_attributes[key].includes(constraint.value) || - node_attributes[key].includes(constraint.value.toString())) { - return true; - } - } else { - if (node_attributes[key] == constraint.value || - node_attributes[key] == constraint.value.toString() || - node_attributes[key] == parseInt(constraint.value)) { - return true; - } - } - } else { - if (Array.isArray(node_attributes[key])) { - if (node_attributes[key].includes(constraint.value)) { - return true; - } - } else { - if (node_attributes[key] == constraint.value || - node_attributes[key] == constraint.value.toString() || - node_attributes[key] == parseInt(constraint.value)) { - return true; - } - } - } - } - return false; - case ">": - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) > parseInt(constraint.value)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) > parseInt(constraint.value)) { - return true; - } - } - } - return false; - case ">=": - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) >= parseInt(constraint.value)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) >= parseInt(constraint.value)) { - return true; - } - } - } - return false; - case "<": - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) > parseInt(constraint.value)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) < parseInt(constraint.value)) { - return true; - } - } - } - return false; - case "<=": - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) <= parseInt(constraint.value)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) <= parseInt(constraint.value)) { - return true; - } - } - } - return false; - default: - debug(`Node operator not handled ${constraint.operator}`); - return false; - }; - } - } - - storeRecords(records) { - debug(`(6) Storing records...`); - //store new records in current edge - this.records = records; - //will update records if any constraints are found - this.applyNodeConstraints(); - debug(`(7) Updating nodes based on edge records...`); - this.updateNodesCuries(records); - } - - getID() { - return this.qEdge.getID(); - } - - getHashedEdgeRepresentation() { - const toBeHashed = - this.getSubject().getCategories() + this.getPredicate() + this.getObject().getCategories() + this.getInputCurie(); - return helper._generateHash(toBeHashed); - } - - expandPredicates(predicates) { - const reducer = (acc, cur) => [...acc, ...biolink.getDescendantPredicates(cur)]; - return Array.from(new Set(predicates.reduce(reducer, []))); - } - - getPredicate() { - if (this.qEdge.predicate === undefined || this.qEdge.predicate === null) { - return undefined; - } - const predicates = utils.toArray(this.qEdge.predicate).map((item) => utils.removeBioLinkPrefix(item)); - const expandedPredicates = this.expandPredicates(predicates); - debug(`Expanded edges: ${expandedPredicates}`); - return expandedPredicates - .map((predicate) => { - return this.isReversed() === true ? biolink.reverse(predicate) : predicate; - }) - .filter((item) => !(typeof item === 'undefined')); - } - - getSubject() { - if (this.reverse) { - return this.qEdge.object; - } - return this.qEdge.subject; - } - - getObject() { - if (this.reverse) { - return this.qEdge.subject; - } - return this.qEdge.object; - } - - isReversed() { - return this.reverse; - } - - getInputCurie() { - let curie = this.qEdge.subject.getCurie() || this.qEdge.object.getCurie(); - if (Array.isArray(curie)) { - return curie; - } - return [curie]; - } - - getInputNode() { - return this.reverse ? this.qEdge.object : this.qEdge.subject; - } - - getOutputNode() { - return this.reverse ? this.qEdge.subject : this.qEdge.object; - } - - hasInputResolved() { - return !(Object.keys(this.input_equivalent_identifiers).length === 0); - } - - hasInput() { - if (this.reverse) { - return this.qEdge.object.hasInput(); - } - return this.qEdge.subject.hasInput(); - } - - getReversedPredicate(predicate) { - return predicate ? biolink.reverse(predicate) : undefined; - } -}; diff --git a/src/query_graph.js b/src/query_graph.js index 28acbb7b..a224386b 100644 --- a/src/query_graph.js +++ b/src/query_graph.js @@ -1,8 +1,6 @@ const QEdge = require('./query_edge'); const InvalidQueryGraphError = require('./exceptions/invalid_query_graph_error'); const LogEntry = require('./log_entry'); -const QueryExecutionEdge = require('./query_execution_edge'); -const MegaQEdge = require('./mega_query_edge'); const debug = require('debug')('bte:biothings-explorer-trapi:query_graph'); const QNode = require('./query_node'); const biolink = require('./biolink'); @@ -308,8 +306,8 @@ module.exports = class QueryGraphHandler { this.nodes[this.queryGraph.edges[qEdgeID].object].updateConnection(qEdgeID); edges[edge_index] = [edge_info.object.curie ? - new MegaQEdge(qEdgeID, edge_info, true) : - new MegaQEdge(qEdgeID, edge_info, false)]; + new QEdge(qEdgeID, edge_info, true) : + new QEdge(qEdgeID, edge_info, false)]; edge_index++; } this.logs.push( From 752d31cfd8086541bbb7468e6ce8fdc99c6b1d07 Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Mon, 3 Oct 2022 17:52:21 -0700 Subject: [PATCH 05/34] remove tests for removed storeEdges function --- .../integration/QueryGraphHandler.test.js | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/__test__/integration/QueryGraphHandler.test.js b/__test__/integration/QueryGraphHandler.test.js index 21f07367..4c9596db 100644 --- a/__test__/integration/QueryGraphHandler.test.js +++ b/__test__/integration/QueryGraphHandler.test.js @@ -241,26 +241,6 @@ describe("Testing QueryGraphHandler Module", () => { }); }); - describe("test _storeEdges function", () => { - - test("test storeEdges with one hop query", async () => { - let handler = new QueryGraphHandler(OneHopQuery); - let edges = await handler._storeEdges(); - expect(edges).toHaveProperty("e01"); - expect(edges).not.toHaveProperty("e02"); - expect(edges.e01).toBeInstanceOf(QEdge); - expect(edges.e01.getSubject()).toBeInstanceOf(QNode2); - }); - - test("test storeEdges with multi hop query", async () => { - let handler = new QueryGraphHandler(FourHopQuery); - let edges = await handler._storeEdges(); - expect(edges).toHaveProperty("e01"); - expect(edges).toHaveProperty("e02"); - expect(edges.e01).toBeInstanceOf(QEdge); - }); - }); - describe("test _createQueryPaths function", () => { test("test createQueryPaths with three hop explain query", async () => { From 610fea30ad92b6e1a696d8fff5210adfb4c8eaed Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Mon, 3 Oct 2022 18:37:56 -0700 Subject: [PATCH 06/34] minor fixes --- __test__/integration/QueryEdge.test.js | 30 ++++++++++++++------------ src/query_edge.js | 5 ++++- src/query_graph.js | 4 +--- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/__test__/integration/QueryEdge.test.js b/__test__/integration/QueryEdge.test.js index 33a96761..76231d5c 100644 --- a/__test__/integration/QueryEdge.test.js +++ b/__test__/integration/QueryEdge.test.js @@ -87,23 +87,25 @@ describe("Testing QueryEdge Module", () => { }) - describe("Testing hasInputResolved function", () => { - test("test return true if subject has input resolved", () => { - const res = edge2.hasInputResolved(); - expect(res).toBeTruthy(); - }); - test("test return false if both subject and object do not have input resolved", () => { - const res = edge1.hasInputResolved(); - expect(res).toBeFalsy(); - }); + // Removed because new QEdge has different implementation for hasInputResolved + // describe("Testing hasInputResolved function", () => { + // test("test return true if subject has input resolved", () => { + // const res = edge2.hasInputResolved(); + // expect(res).toBeTruthy(); + // }); - test("test return true if subject doesn't have input resolved, but object does", () => { - const res = edge5.hasInputResolved(); - expect(res).toBeTruthy(); - }); + // test("test return false if both subject and object do not have input resolved", () => { + // const res = edge1.hasInputResolved(); + // expect(res).toBeFalsy(); + // }); - }) + // test("test return true if subject doesn't have input resolved, but object does", () => { + // const res = edge5.hasInputResolved(); + // expect(res).toBeTruthy(); + // }); + + // }) describe("Testing getPredicate function", () => { test("test get reverse predicate if query is reversed", () => { diff --git a/src/query_edge.js b/src/query_edge.js index 283f1f1b..ac3893db 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -12,12 +12,15 @@ module.exports = class QEdge { * @param {object} info - QEdge info, e.g. subject, object, predicate * @param {boolean} reverse - is QEdge reversed? */ - constructor(id, info, reverse = false) { + constructor(id, info, reverse = null) { this.id = id; this.predicate = info.predicates; this.subject = info.frozen === true ? QNode.unfreeze(info.subject) : info.subject; this.object = info.frozen === true ? QNode.unfreeze(info.object) : info.object; this.expanded_predicates = []; + + if (reverse === null) reverse = !(info.subject.curie) && !!(info.object.curie); + this.init(); this.reverse = reverse; diff --git a/src/query_graph.js b/src/query_graph.js index a224386b..9cc71d16 100644 --- a/src/query_graph.js +++ b/src/query_graph.js @@ -305,9 +305,7 @@ module.exports = class QueryGraphHandler { this.nodes[this.queryGraph.edges[qEdgeID].subject].updateConnection(qEdgeID); this.nodes[this.queryGraph.edges[qEdgeID].object].updateConnection(qEdgeID); - edges[edge_index] = [edge_info.object.curie ? - new QEdge(qEdgeID, edge_info, true) : - new QEdge(qEdgeID, edge_info, false)]; + edges[edge_index] = [new QEdge(qEdgeID, edge_info)]; edge_index++; } this.logs.push( From fd43ae251de6322821735109ba10e52112953f84 Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Mon, 10 Oct 2022 17:05:12 -0700 Subject: [PATCH 07/34] fix some tests --- src/query_edge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/query_edge.js b/src/query_edge.js index ac3893db..c673f07f 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -19,7 +19,7 @@ module.exports = class QEdge { this.object = info.frozen === true ? QNode.unfreeze(info.object) : info.object; this.expanded_predicates = []; - if (reverse === null) reverse = !(info.subject.curie) && !!(info.object.curie); + if (reverse === null) reverse = !(info.subject?.getCurie()) && !!(info.object?.getCurie()); this.init(); From f519789fab16130fd22a360753a3b8cdd19eeb56 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Fri, 14 Oct 2022 15:16:39 -0400 Subject: [PATCH 08/34] feat: support x-bte qualifiers --- src/graph/graph.js | 3 +++ src/graph/kg_edge.js | 5 +++++ src/graph/knowledge_graph.js | 33 ++++++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/graph/graph.js b/src/graph/graph.js index c5916066..dc77f66f 100644 --- a/src/graph/graph.js +++ b/src/graph/graph.js @@ -64,6 +64,9 @@ module.exports = class BTEGraph { .map((item) => { this.edges[recordHash].addAdditionalAttributes(item, record.mappedResponse[item]); }); + Object.entries(record.qualifiers).forEach(([qualifierType, qualifier]) => { + this.edges[recordHash].addQualifier(qualifierType, qualifier); + }) } }); } diff --git a/src/graph/kg_edge.js b/src/graph/kg_edge.js index bc18dd9a..949eef1b 100644 --- a/src/graph/kg_edge.js +++ b/src/graph/kg_edge.js @@ -8,6 +8,7 @@ module.exports = class KGEdge { this.inforesCuries = new Set(); this.sources = new Set(); this.publications = new Set(); + this.qualifiers = {}; this.attributes = {}; } @@ -59,6 +60,10 @@ module.exports = class KGEdge { }); } + addQualifier(name, value) { + this.qualifiers[name] = value; + } + addAdditionalAttributes(name, value) { this.attributes[name] = value; } diff --git a/src/graph/knowledge_graph.js b/src/graph/knowledge_graph.js index 68e7273d..96d8d38f 100644 --- a/src/graph/knowledge_graph.js +++ b/src/graph/knowledge_graph.js @@ -64,6 +64,17 @@ module.exports = class KnowledgeGraph { return res; } + _createQualifiers(kgEdge) { + const qualifiers = Object.entries(kgEdge.qualifiers).map(([qualifierType, qualifier]) => { + return { + qualifier_type_id: qualifierType, + qualifier_value: qualifier, + }; + }); + + return qualifiers.length ? qualifiers : undefined; + } + _createAttributes(kgEdge) { let attributes = [ { @@ -73,9 +84,11 @@ module.exports = class KnowledgeGraph { }, ]; - if (kgEdge.attributes['edge-attributes']) { //handle TRAPI APIs (Situation A of https://github.com/biothings/BioThings_Explorer_TRAPI/issues/208) and APIs that define 'edge-atributes' in x-bte + if (kgEdge.attributes['edge-attributes']) { + //handle TRAPI APIs (Situation A of https://github.com/biothings/BioThings_Explorer_TRAPI/issues/208) and APIs that define 'edge-atributes' in x-bte attributes = [...attributes, ...kgEdge.attributes['edge-attributes']]; - } else if ( //handle direct info providers (Situation C of https://github.com/biothings/BioThings_Explorer_TRAPI/issues/208) + } else if ( + //handle direct info providers (Situation C of https://github.com/biothings/BioThings_Explorer_TRAPI/issues/208) [ 'Clinical Risk KP API', 'Text Mining Targeted Association API', @@ -94,7 +107,7 @@ module.exports = class KnowledgeGraph { attribute_type_id: 'biolink:primary_knowledge_source', value: Array.from(kgEdge.sources), value_type_id: 'biolink:InformationResource', - } + }, ]; } //aggregator knowledge source @@ -105,7 +118,7 @@ module.exports = class KnowledgeGraph { attribute_type_id: 'biolink:aggregator_knowledge_source', value: Array.from(kgEdge.inforesCuries), value_type_id: 'biolink:InformationResource', - } + }, ]; } //publications @@ -116,7 +129,7 @@ module.exports = class KnowledgeGraph { attribute_type_id: 'biolink:publications', value: Array.from(kgEdge.publications), // value_type_id: 'biolink:publications', - } + }, ]; } @@ -127,7 +140,8 @@ module.exports = class KnowledgeGraph { //value_type_id: 'bts:' + key, }); } - } else { //handle non-trapi APIs (Situation B of https://github.com/biothings/BioThings_Explorer_TRAPI/issues/208) + } else { + //handle non-trapi APIs (Situation B of https://github.com/biothings/BioThings_Explorer_TRAPI/issues/208) attributes = [...attributes]; //primary knowledge source if (Array.from(kgEdge.sources).length) { @@ -137,7 +151,7 @@ module.exports = class KnowledgeGraph { attribute_type_id: 'biolink:primary_knowledge_source', value: Array.from(kgEdge.sources), value_type_id: 'biolink:InformationResource', - } + }, ]; } //aggregator knowledge source @@ -148,7 +162,7 @@ module.exports = class KnowledgeGraph { attribute_type_id: 'biolink:aggregator_knowledge_source', value: Array.from(kgEdge.inforesCuries), value_type_id: 'biolink:InformationResource', - } + }, ]; } //publications @@ -159,7 +173,7 @@ module.exports = class KnowledgeGraph { attribute_type_id: 'biolink:publications', value: Array.from(kgEdge.publications), // value_type_id: 'biolink:publications', - } + }, ]; } @@ -180,6 +194,7 @@ module.exports = class KnowledgeGraph { predicate: kgEdge.predicate, subject: kgEdge.subject, object: kgEdge.object, + qualifiers: this._createQualifiers(kgEdge), attributes: this._createAttributes(kgEdge), }; } From 8a52c3a4f8d6a20e9593998565e13674107f0dd3 Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Fri, 14 Oct 2022 18:19:26 -0700 Subject: [PATCH 09/34] use input/output node (needs more testing) --- src/batch_edge_query.js | 4 ++-- src/qedge2apiedge.js | 20 ++++++++++---------- src/query_edge.js | 13 +++++-------- src/query_node.js | 4 ++++ src/update_nodes.js | 4 ++-- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/batch_edge_query.js b/src/batch_edge_query.js index 8a7a6f0e..cea79a6a 100644 --- a/src/batch_edge_query.js +++ b/src/batch_edge_query.js @@ -78,7 +78,7 @@ module.exports = class BatchEdgeQueryHandler { node.curie.forEach((curie) => { // if the curie is already present, or an equivalent is, remove it if (!reducedCuries.includes(curie)) { - const equivalentAlreadyIncluded = qXEdge.input_equivalent_identifiers[curie][0].curies.some( + const equivalentAlreadyIncluded = qXEdge.getInputNode().getEquivalentIDs()[curie][0].curies.some( (equivalentCurie) => reducedCuries.includes(equivalentCurie), ); if (!equivalentAlreadyIncluded) { @@ -99,7 +99,7 @@ module.exports = class BatchEdgeQueryHandler { } }); strippedCuries.forEach((curie) => { - delete qXEdge.input_equivalent_identifiers[curie]; + qXEdge.getInputNode().removeEquivalentID(curie); }); }); } diff --git a/src/qedge2apiedge.js b/src/qedge2apiedge.js index a010c283..4135b568 100644 --- a/src/qedge2apiedge.js +++ b/src/qedge2apiedge.js @@ -32,20 +32,20 @@ module.exports = class QEdge2APIEdgeHandler { * @param {object} qXEdge - TRAPI Query Edge Object */ getMetaXEdges(qXEdge, metaKG = this.metaKG) { - debug(`Subject node is ${qXEdge.getSubject().id}`); - debug(`Object node is ${qXEdge.getObject().id}`); + debug(`Input node is ${qXEdge.getInputNode().id}`); + debug(`Output node is ${qXEdge.getOutputNode().id}`); this.logs.push( new LogEntry( 'DEBUG', null, - `BTE is trying to find metaKG edges (smartAPI registry, x-bte annotation) connecting from ${qXEdge.getSubject().getCategories()} to ${qXEdge - .getObject() + `BTE is trying to find metaKG edges (smartAPI registry, x-bte annotation) connecting from ${qXEdge.getInputNode().getCategories()} to ${qXEdge + .getOutputNode() .getCategories()} with predicate ${qXEdge.getPredicate()}`, ).getLog(), ); let filterCriteria = { - input_type: qXEdge.getSubject().getCategories(), - output_type: qXEdge.getObject().getCategories(), + input_type: qXEdge.getInputNode().getCategories(), + output_type: qXEdge.getOutputNode().getCategories(), predicate: qXEdge.getPredicate(), }; debug(`KG Filters: ${JSON.stringify(filterCriteria, null, 2)}`); @@ -83,7 +83,7 @@ module.exports = class QEdge2APIEdgeHandler { const APIEdges = []; const inputPrefix = metaXEdge.association.input_id; const inputType = metaXEdge.association.input_type; - const resolvedCuries = metaXEdge.reasoner_edge.input_equivalent_identifiers; + const resolvedCuries = metaXEdge.reasoner_edge.getInputNode().getEquivalentIDs(); for (const curie in resolvedCuries) { await Promise.all(resolvedCuries[curie].map(async (entity) => { if (entity.semanticType === inputType && inputPrefix in entity.dbIDs) { @@ -134,7 +134,7 @@ module.exports = class QEdge2APIEdgeHandler { const input_resolved_identifiers = {}; const inputPrefix = metaXEdge.association.input_id; const inputType = metaXEdge.association.input_type; - let resolvedCuries = metaXEdge.reasoner_edge.input_equivalent_identifiers; + let resolvedCuries = metaXEdge.reasoner_edge.getInputNode().getEquivalentIDs(); // debug(`Resolved ids: ${JSON.stringify(resolvedIDs)}`); debug(`Input prefix: ${inputPrefix}`); for (const curie in resolvedCuries) { @@ -205,7 +205,7 @@ module.exports = class QEdge2APIEdgeHandler { const APIEdges = []; const inputPrefix = metaXEdge.association.input_id; const inputType = metaXEdge.association.input_type; - const resolvedCuries = metaXEdge.reasoner_edge.input_equivalent_identifiers; + const resolvedCuries = metaXEdge.reasoner_edge.getInputNode().getEquivalentIDs() for (const curie in resolvedCuries) { resolvedCuries[curie].map((entity) => { if (entity.semanticType === inputType && inputPrefix in entity.dbIDs) { @@ -246,7 +246,7 @@ module.exports = class QEdge2APIEdgeHandler { const input_resolved_identifiers = {}; const inputPrefix = metaXEdge.association.input_id; const inputType = metaXEdge.association.input_type; - let resolvedCuries = metaXEdge.reasoner_edge.input_equivalent_identifiers; + let resolvedCuries = metaXEdge.reasoner_edge.getInputNode().getEquivalentIDs(); // debug(`Resolved ids: ${JSON.stringify(resolvedIDs)}`); debug(`Input prefix: ${inputPrefix}`); for (const curie in resolvedCuries) { diff --git a/src/query_edge.js b/src/query_edge.js index c673f07f..05baf767 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -24,9 +24,7 @@ module.exports = class QEdge { this.init(); this.reverse = reverse; - //object and subject aliases - this.input_equivalent_identifiers = info.input_equivalent_identifiers === undefined ? {} : info.input_equivalent_identifiers; - this.output_equivalent_identifiers = info.output_equivalent_identifiers === undefined ? {} : info.output_equivalent_identifiers; + //edge has been fully executed this.executed = info.executed === undefined ? false : info.executed; //run initial checks @@ -44,7 +42,6 @@ module.exports = class QEdge { expanded_predicates: this.expanded_predicates, executed: this.executed, reverse: this.reverse, - input_equivalent_identifiers: this.input_equivalent_identifiers, logs: this.logs, subject: this.subject.freeze(), object: this.object.freeze(), @@ -484,7 +481,7 @@ module.exports = class QEdge { getHashedEdgeRepresentation() { const toBeHashed = - this.getSubject().getCategories() + this.getPredicate() + this.getObject().getCategories() + this.getInputCurie(); + this.getInputNode().getCategories() + this.getPredicate() + this.getOutputNode().getCategories() + this.getInputCurie(); return helper._generateHash(toBeHashed); } @@ -507,14 +504,14 @@ module.exports = class QEdge { .filter((item) => !(typeof item === 'undefined')); } - getSubject() { + getInputNode() { if (this.reverse) { return this.object; } return this.subject; } - getObject() { + getOutputNode() { if (this.reverse) { return this.subject; } @@ -542,7 +539,7 @@ module.exports = class QEdge { } hasInputResolved() { - return !(Object.keys(this.input_equivalent_identifiers).length === 0); + return this.getInputNode().hasEquivalentIDs(); } hasInput() { diff --git a/src/query_node.js b/src/query_node.js index aa18da7e..39528b40 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -185,6 +185,10 @@ module.exports = class QNode { return this.equivalentIDs; } + removeEquivalentID(id) { + delete this.equivalentIDs[id]; + } + getCategories() { if (this.hasEquivalentIDs() === false) { const categories = utils.toArray(this.category); diff --git a/src/update_nodes.js b/src/update_nodes.js index 9f62f15e..c7e0c05b 100644 --- a/src/update_nodes.js +++ b/src/update_nodes.js @@ -17,7 +17,7 @@ module.exports = class NodesUpdateHandler { return; } if (qXEdge.hasInput()) { - const inputCategories = qXEdge.getSubject().getCategories(); + const inputCategories = qXEdge.getInputNode().getCategories(); inputCategories.map((category) => { if (!(category in curies)) { curies[category] = []; @@ -53,7 +53,7 @@ module.exports = class NodesUpdateHandler { }, {}); debug(`Got Edge Equivalent IDs successfully.`); if (Object.keys(edgeEquivalentIDs).length > 0) { - qXEdge.input_equivalent_identifiers = edgeEquivalentIDs; + qXEdge.getInputNode().setEquivalentIDs(edgeEquivalentIDs); } }); return; From 11446ecede0d8d9f309a051e9cdab780844ece32 Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Wed, 19 Oct 2022 17:44:21 -0700 Subject: [PATCH 10/34] Fixes for Input/Output --- src/query_edge.js | 1 - src/query_node.js | 24 ++++++++++++------------ src/update_nodes.js | 5 ++--- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/query_edge.js b/src/query_edge.js index 05baf767..a13c6890 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -45,7 +45,6 @@ module.exports = class QEdge { logs: this.logs, subject: this.subject.freeze(), object: this.object.freeze(), - output_equivalent_identifiers: this.output_equivalent_identifiers, predicate: this.predicate, records: this.records.map(record => record.freeze()) }; diff --git a/src/query_node.js b/src/query_node.js index 39528b40..eeedf042 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -182,7 +182,7 @@ module.exports = class QNode { } getEquivalentIDs() { - return this.equivalentIDs; + return this.equivalentIDs ?? {}; } removeEquivalentID(id) { @@ -191,20 +191,20 @@ module.exports = class QNode { getCategories() { if (this.hasEquivalentIDs() === false) { - const categories = utils.toArray(this.category); - let expanded_categories = []; - categories.map((category) => { - expanded_categories = [ - ...expanded_categories, - ...biolink.getDescendantClasses(utils.removeBioLinkPrefix(category)), - ]; - }); - return utils.getUnique(expanded_categories); + const categories = utils.toArray(this.category); + let expanded_categories = []; + categories.map((category) => { + expanded_categories = [ + ...expanded_categories, + ...biolink.getDescendantClasses(utils.removeBioLinkPrefix(category)), + ]; + }); + return utils.getUnique(expanded_categories); } let categories = []; Object.values(this.equivalentIDs).map((entities) => { entities.map((entity) => { - categories = [...categories, ...entity.semanticTypes]; + categories = [...categories, ...entity.semanticTypes.map(semantic => utils.removeBioLinkPrefix(semantic))]; }); }); return utils.getUnique(categories); @@ -237,7 +237,7 @@ module.exports = class QNode { } hasEquivalentIDs() { - return !(typeof this.equivalentIDs === 'undefined'); + return !(typeof this.equivalentIDs === 'undefined' || this.equivalentIDs === {}); } getEntityCount() { diff --git a/src/update_nodes.js b/src/update_nodes.js index c7e0c05b..c530c459 100644 --- a/src/update_nodes.js +++ b/src/update_nodes.js @@ -82,10 +82,9 @@ module.exports = class NodesUpdateHandler { queryRecords.map((record) => { if ( record && - !(record.object.curie in record.qXEdge.output_equivalent_identifiers) + !(record.object.curie in record.qXEdge.getOutputNode().getEquivalentIDs()) ) { - record.qXEdge.output_equivalent_identifiers[record.object.curie] = - record.object.normalizedInfo; + record.qXEdge.getOutputNode().updateEquivalentIDs({[record.object.curie]: record.object.normalizedInfo}); } }); } From 22bdeed593fd860eb685975cb2eb09aa9aa6a8ef Mon Sep 17 00:00:00 2001 From: Rohan Juneja Date: Thu, 20 Oct 2022 18:01:33 -0700 Subject: [PATCH 11/34] Move unfreeze to constructor --- .../integration/QEdge2BTEEdgeHandler.test.js | 16 ++--- __test__/integration/QueryEdge.test.js | 44 ++++++------- __test__/integration/QueryNode.test.js | 28 ++++---- __test__/integration/QueryResult.test.js | 66 +++++++++---------- __test__/unittest/QueryEdge.test.js | 18 +++-- src/query_edge.js | 26 ++++---- src/query_graph.js | 6 +- src/query_node.js | 12 +--- 8 files changed, 107 insertions(+), 109 deletions(-) diff --git a/__test__/integration/QEdge2BTEEdgeHandler.test.js b/__test__/integration/QEdge2BTEEdgeHandler.test.js index 15301cd9..99cdd2b5 100644 --- a/__test__/integration/QEdge2BTEEdgeHandler.test.js +++ b/__test__/integration/QEdge2BTEEdgeHandler.test.js @@ -3,7 +3,7 @@ const QEdge = require("../../src/query_edge"); const NodeUpdateHandler = require("../../src/update_nodes"); describe("Testing NodeUpdateHandler Module", () => { - const gene_node1 = new QNode("n1", { categories: ["Gene"], ids: ["NCBIGene:1017"] }); + const gene_node1 = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; const node1_equivalent_ids = { "NCBIGene:1017": { db_ids: { @@ -13,15 +13,15 @@ describe("Testing NodeUpdateHandler Module", () => { } } - const gene_node2 = new QNode("n2", { categories: ["Gene"], ids: ["NCBIGene:1017", "NCBIGene:1018"] }); - const gene_node1_with_id_annotated = new QNode("n1", { categories: ["Gene"], ids: ["NCBIGene:1017"] }); + const gene_node2 = new QNode({ id: "n2", categories: ["Gene"], ids: ["NCBIGene:1017", "NCBIGene:1018"] }) ; + const gene_node1_with_id_annotated = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; gene_node1_with_id_annotated.setEquivalentIDs(node1_equivalent_ids); //gene_node2.setEquivalentIDs(node2_equivalent_ids); - const chemical_node1 = new QNode("n3", { categories:[ "SmallMolecule"] }); - const edge1 = new QEdge("e01", { subject: gene_node1, object: chemical_node1 }); - const edge2 = new QEdge("e02", { subject: gene_node1_with_id_annotated, object: chemical_node1 }); - const edge3 = new QEdge('e04', { subject: gene_node2, object: chemical_node1 }); - const edge4 = new QEdge('e05', { object: gene_node2, subject: chemical_node1 }); + const chemical_node1 = new QNode({ id: "n3", categories:[ "SmallMolecule"] }) ; + const edge1 = new QEdge({ id: "e01", subject: gene_node1, object: chemical_node1 }); + const edge2 = new QEdge({ id: "e02", subject: gene_node1_with_id_annotated, object: chemical_node1 }); + const edge3 = new QEdge({ id: 'e04', subject: gene_node2, object: chemical_node1 }); + const edge4 = new QEdge({ id: 'e05', object: gene_node2, subject: chemical_node1 }); describe("Testing _getCuries function", () => { test("test edge with one curie input return an array of one", () => { diff --git a/__test__/integration/QueryEdge.test.js b/__test__/integration/QueryEdge.test.js index 76231d5c..84a98147 100644 --- a/__test__/integration/QueryEdge.test.js +++ b/__test__/integration/QueryEdge.test.js @@ -2,9 +2,9 @@ const QNode = require("../../src/query_node"); const QEdge = require("../../src/query_edge"); describe("Testing QueryEdge Module", () => { - const gene_node1 = new QNode("n1", { categories: ["Gene"], ids: ["NCBIGene:1017"] }); - const type_node = new QNode("n2", { categories: ["SmallMolecule"] }); - const disease1_node = new QNode("n1", { categories: ["Disease"], ids: ["MONDO:000123"] }); + const gene_node1 = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; + const type_node = new QNode({ id: "n2", categories: ["SmallMolecule"] }) ; + const disease1_node = new QNode({ id: "n1", categories: ["Disease"], ids: ["MONDO:000123"] }) ; const node1_equivalent_ids = { "NCBIGene:1017": { db_ids: { @@ -14,15 +14,15 @@ describe("Testing QueryEdge Module", () => { } } - const gene_node2 = new QNode("n2", { categories: "Gene", ids: ["NCBIGene:1017", "NCBIGene:1018"] }); - const gene_node1_with_id_annotated = new QNode("n1", { categories: ["Gene"], ids: ["NCBIGene:1017"] }); + const gene_node2 = new QNode({ id: "n2", categories: "Gene", ids: ["NCBIGene:1017", "NCBIGene:1018"] }) ; + const gene_node1_with_id_annotated = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; gene_node1_with_id_annotated.setEquivalentIDs(node1_equivalent_ids); - const chemical_node1 = new QNode("n3", { categories: ["SmallMolecule"] }); - const edge1 = new QEdge("e01", { subject: gene_node1, object: chemical_node1 }); - const edge2 = new QEdge("e02", { subject: gene_node1_with_id_annotated, object: chemical_node1 }); - const edge3 = new QEdge('e04', { subject: gene_node2, object: chemical_node1 }); - const edge4 = new QEdge('e05', { object: gene_node2, subject: chemical_node1 }); - const edge5 = new QEdge('e06', { object: gene_node1_with_id_annotated, subject: chemical_node1 }); + const chemical_node1 = new QNode({ id: "n3", categories: ["SmallMolecule"] }) ; + const edge1 = new QEdge({ id: "e01", subject: gene_node1, object: chemical_node1 }); + const edge2 = new QEdge({ id: "e02", subject: gene_node1_with_id_annotated, object: chemical_node1 }); + const edge3 = new QEdge({ id: 'e04', subject: gene_node2, object: chemical_node1 }); + const edge4 = new QEdge({ id: 'e05', object: gene_node2, subject: chemical_node1 }); + const edge5 = new QEdge({ id: 'e06', object: gene_node1_with_id_annotated, subject: chemical_node1 }); describe("Testing isReversed function", () => { test("test if only the object of the edge has curie defined, should return true", () => { @@ -36,9 +36,9 @@ describe("Testing QueryEdge Module", () => { }); test("test if both subject and object curie not defined, should return false", () => { - const node1 = new QNode("n1", { categories: ["Gene"] }); - const node2 = new QNode("n2", { categories: ["SmallMolecule"] }); - const edge = new QEdge("e01", { subject: node1, object: node2 }); + const node1 = new QNode({ id: "n1", categories: ["Gene"] }) ; + const node2 = new QNode({ id: "n2", categories: ["SmallMolecule"] }) ; + const edge = new QEdge({ id: "e01", subject: node1, object: node2 }); expect(edge.isReversed()).toBeFalsy(); }); @@ -79,9 +79,9 @@ describe("Testing QueryEdge Module", () => { }); test("test return false if both subject and object has no curies specified", () => { - const node1 = new QNode("n1", { categories: ["Gene"] }); - const node2 = new QNode("n2", { categories: ["SmallMolecule"] }); - const edge = new QEdge("e01", { subject: node1, object: node2 }); + const node1 = new QNode({ id: "n1", categories: ["Gene"] }) ; + const node2 = new QNode({ id: "n2", categories: ["SmallMolecule"] }) ; + const edge = new QEdge({ id: "e01", subject: node1, object: node2 }); expect(edge.hasInput()).toBeFalsy(); }); @@ -109,13 +109,13 @@ describe("Testing QueryEdge Module", () => { describe("Testing getPredicate function", () => { test("test get reverse predicate if query is reversed", () => { - const edge = new QEdge("e01", { subject: type_node, object: disease1_node, predicates: ["biolink:treats"] }); + const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: ["biolink:treats"] }); const res = edge.getPredicate(); expect(res).toContain("treated_by"); }); test("test get reverse predicate if query is reversed and expanded", () => { - const edge = new QEdge("e01", { subject: type_node, object: disease1_node, predicates: ["biolink:affects"] }); + const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: ["biolink:affects"] }); const res = edge.getPredicate(); expect(res).toContain("affected_by"); expect(res).toContain("disrupted_by"); @@ -124,14 +124,14 @@ describe("Testing QueryEdge Module", () => { describe("Testing expandPredicates function", () => { test("All predicates are correctly expanded if in biolink model", () => { - const edge = new QEdge("e01", { subject: type_node, object: disease1_node, predicates: ["biolink:contributes_to"] }); + const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: ["biolink:contributes_to"] }); const res = edge.expandPredicates(["contributes_to"]) expect(res).toContain("contributes_to"); expect(res).toContain("causes"); }); test("Multiple predicates can be resolved", () => { - const edge = new QEdge("e01", { subject: type_node, object: disease1_node, predicates: ["biolink:contributes_to"] }); + const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: ["biolink:contributes_to"] }); const res = edge.expandPredicates(["contributes_to", "ameliorates"]) expect(res).toContain("contributes_to"); expect(res).toContain("causes"); @@ -140,7 +140,7 @@ describe("Testing QueryEdge Module", () => { }); test("Predicates not in biolink model should return itself", () => { - const edge = new QEdge("e01", { subject: type_node, object: disease1_node, predicates: "biolink:contributes_to" }); + const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: "biolink:contributes_to" }); const res = edge.expandPredicates(["contributes_to", "amelio"]) expect(res).toContain("contributes_to"); expect(res).toContain("causes"); diff --git a/__test__/integration/QueryNode.test.js b/__test__/integration/QueryNode.test.js index 24fc946c..1067822d 100644 --- a/__test__/integration/QueryNode.test.js +++ b/__test__/integration/QueryNode.test.js @@ -12,13 +12,13 @@ describe("Testing QueryNode Module", () => { describe("Testing hasInput function", () => { test("test node without curies specified should return false", () => { - const gene_node = new QNode("n1", { categories: ["Gene"] }); + const gene_node = new QNode({ id: "n1", categories: ["Gene"] }) ; const res = gene_node.hasInput(); expect(res).toBeFalsy(); }) test("test node with curies specified should return true", () => { - const gene_node = new QNode("n1", { categories: ["Gene"], ids: ["NCBIGene:1017"] }); + const gene_node = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; const res = gene_node.hasInput(); expect(res).toBeTruthy(); }) @@ -26,14 +26,14 @@ describe("Testing QueryNode Module", () => { describe("Test hasEquivalentIDs function", () => { test("test node with equivalent identifiers set should return true", () => { - const gene_node = new QNode("n1", { categories: ["Gene"] }); + const gene_node = new QNode({ id: "n1", categories: ["Gene"] }) ; gene_node.setEquivalentIDs(node1_equivalent_ids); const res = gene_node.hasEquivalentIDs(); expect(res).toBeTruthy(); }); test("test node with equivalent identifiers not set should return false", () => { - const gene_node = new QNode("n1", { categories: "Gene" }); + const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; const res = gene_node.hasEquivalentIDs(); expect(res).toBeFalsy(); }) @@ -41,13 +41,13 @@ describe("Testing QueryNode Module", () => { describe("Test getEntities", () => { test("If equivalent ids are empty, should return an empty array", () => { - const gene_node = new QNode("n1", { categories: "Gene" }); + const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; gene_node.equivalentIDs = {}; expect(gene_node.getEntities()).toEqual([]); }) test("If equivalent ids are not empty, should return an array of bioentities", () => { - const gene_node = new QNode("n1", { categories: "Gene" }); + const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; gene_node.equivalentIDs = { "A": [ { @@ -79,13 +79,13 @@ describe("Testing QueryNode Module", () => { describe("Test getPrimaryIDs", () => { test("If equivalent ids are empty, should return an empty array", () => { - const gene_node = new QNode("n1", { categories: "Gene" }); + const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; gene_node.equivalentIDs = {}; expect(gene_node.getPrimaryIDs()).toEqual([]); }) test("If equivalent ids are not empty, should return an array of primaryIDs", () => { - const gene_node = new QNode("n1", { categories: "Gene" }); + const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; gene_node.equivalentIDs = { "A": [ { @@ -107,13 +107,13 @@ describe("Testing QueryNode Module", () => { describe("Test updateEquivalentIDs", () => { test("If equivalent ids does not exist, should set it with the input", () => { - const gene_node = new QNode("n1", { categories: "Gene" }); + const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; gene_node.updateEquivalentIDs({ "a": "b" }) expect(gene_node.equivalentIDs).toEqual({ "a": "b" }); }) test("If equivalent ids are not empty, should update the equivalent ids", () => { - const gene_node = new QNode("n1", { categories: "Gene" }); + const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; gene_node.equivalentIDs = { "a": "b", "c": "d" }; gene_node.updateEquivalentIDs({ "e": "f" }) expect(gene_node.getEquivalentIDs()).toEqual({ "a": "b", "c": "d", "e": "f" }) @@ -122,14 +122,14 @@ describe("Testing QueryNode Module", () => { describe("Test getCategories function", () => { test("If equivalent ids are empty, return itself and its descendants", () => { - const node = new QNode("n1", { categories: "DiseaseOrPhenotypicFeature" }); + const node = new QNode({ id: "n1", categories: "DiseaseOrPhenotypicFeature" }) ; expect(node.getCategories()).toContain("Disease"); expect(node.getCategories()).toContain("PhenotypicFeature"); expect(node.getCategories()).toContain("DiseaseOrPhenotypicFeature"); }) test("If equivalent ids are empty, return itself and its descendants using NamedThing as example", () => { - const node = new QNode("n1", { categories: "NamedThing" }); + const node = new QNode({ id: "n1", categories: "NamedThing" }) ; expect(node.getCategories()).toContain("Disease"); expect(node.getCategories()).toContain("PhenotypicFeature"); expect(node.getCategories()).toContain("DiseaseOrPhenotypicFeature"); @@ -138,12 +138,12 @@ describe("Testing QueryNode Module", () => { }) test("If equivalent ids are empty, return itself and its descendants using Gene as example", () => { - const node = new QNode("n1", { categories: "Gene" }); + const node = new QNode({ id: "n1", categories: "Gene" }) ; expect(node.getCategories()).toEqual(["Gene"]) }) test("If equivalent ids are not empty, return all semantic types defined in the entity", () => { - const node = new QNode("n1", { categories: "Gene" }); + const node = new QNode({ id: "n1", categories: "Gene" }) ; node.equivalentIDs = { "A": [ { diff --git a/__test__/integration/QueryResult.test.js b/__test__/integration/QueryResult.test.js index f2aea5e4..da1a3213 100644 --- a/__test__/integration/QueryResult.test.js +++ b/__test__/integration/QueryResult.test.js @@ -8,9 +8,9 @@ const config = require('../../src/config.js'); describe('Testing QueryResults Module', () => { describe('"Real" Records', () => { describe('Single Record', () => { - const gene_node1 = new QNode('n1', { categories: ['Gene'], ids: ['NCBIGene:632'] }); - const chemical_node1 = new QNode('n2', { categories: ['ChemicalSubstance'] }); - const edge1 = new QEdge('e01', { subject: gene_node1, object: chemical_node1 }); + const gene_node1 = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:632'] }) ; + const chemical_node1 = new QNode({ id: 'n2', categories: ['ChemicalSubstance'] }) ; + const edge1 = new QEdge({ id: 'e01', subject: gene_node1, object: chemical_node1 }); const record = new Record( { publications: ['PMID:8366144', 'PMID:8381250'], @@ -76,12 +76,12 @@ describe('Testing QueryResults Module', () => { describe('Two Records', () => { describe('query graph: gene1-disease1-gene1', () => { - const gene_node_start = new QNode('n1', { categories: ['Gene'] }); - const disease_node = new QNode('n2', { categories: ['Disease'] }); - const gene_node_end = new QNode('n3', { categories: ['Gene'] }); + const gene_node_start = new QNode({ id: 'n1', categories: ['Gene'] }) ; + const disease_node = new QNode({ id: 'n2', categories: ['Disease'] }) ; + const gene_node_end = new QNode({ id: 'n3', categories: ['Gene'] }) ; - const edge1 = new QEdge('e01', { subject: gene_node_start, object: disease_node }); - const edge2 = new QEdge('e02', { subject: disease_node, object: gene_node_end }); + const edge1 = new QEdge({ id: 'e01', subject: gene_node_start, object: disease_node }); + const edge2 = new QEdge({ id: 'e02', subject: disease_node, object: gene_node_end }); const record1 = new Record( { @@ -197,12 +197,12 @@ describe('Testing QueryResults Module', () => { }); describe('query graph: gene1-disease1-gene2 (no ids params)', () => { - const gene_node_start = new QNode('n1', { categories: ['Gene'] }); - const disease_node = new QNode('n2', { categories: ['Disease'] }); - const gene_node_end = new QNode('n3', { categories: ['Gene'] }); + const gene_node_start = new QNode({ id: 'n1', categories: ['Gene'] }) ; + const disease_node = new QNode({ id: 'n2', categories: ['Disease'] }) ; + const gene_node_end = new QNode({ id: 'n3', categories: ['Gene'] }) ; - const edge1 = new QEdge('e01', { subject: gene_node_start, object: disease_node }); - const edge2 = new QEdge('e02', { subject: disease_node, object: gene_node_end }); + const edge1 = new QEdge({ id: 'e01', subject: gene_node_start, object: disease_node }); + const edge2 = new QEdge({ id: 'e02', subject: disease_node, object: gene_node_end }); const record1 = new Record( { @@ -317,12 +317,12 @@ describe('Testing QueryResults Module', () => { }); describe('query graph: gene1-disease1-gene2 (gene1 has ids param)', () => { - const gene_node_start = new QNode('n1', { categories: ['Gene'], ids: ['NCBIGene:3778'] }); - const disease_node = new QNode('n2', { categories: ['Disease'] }); - const gene_node_end = new QNode('n3', { categories: ['Gene'] }); + const gene_node_start = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:3778'] }) ; + const disease_node = new QNode({ id: 'n2', categories: ['Disease'] }) ; + const gene_node_end = new QNode({ id: 'n3', categories: ['Gene'] }) ; - const edge1 = new QEdge('e01', { subject: gene_node_start, object: disease_node }); - const edge2 = new QEdge('e02', { subject: disease_node, object: gene_node_end }); + const edge1 = new QEdge({ id: 'e01', subject: gene_node_start, object: disease_node }); + const edge2 = new QEdge({ id: 'e02', subject: disease_node, object: gene_node_end }); const record1 = new Record( { @@ -437,12 +437,12 @@ describe('Testing QueryResults Module', () => { }); describe('query graph: gene1-disease1-gene2 (gene1 & gene2 have ids params)', () => { - const gene_node_start = new QNode('n1', { categories: ['Gene'], ids: ['NCBIGene:3778'] }); - const disease_node = new QNode('n2', { categories: ['Disease'] }); - const gene_node_end = new QNode('n3', { categories: ['Gene'], ids: ['NCBIGene:7289'] }); + const gene_node_start = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:3778'] }) ; + const disease_node = new QNode({ id: 'n2', categories: ['Disease'] }) ; + const gene_node_end = new QNode({ id: 'n3', categories: ['Gene'], ids: ['NCBIGene:7289'] }) ; - const edge1 = new QEdge('e01', { subject: gene_node_start, object: disease_node }); - const edge2 = new QEdge('e02', { subject: disease_node, object: gene_node_end }); + const edge1 = new QEdge({ id: 'e01', subject: gene_node_start, object: disease_node }); + const edge2 = new QEdge({ id: 'e02', subject: disease_node, object: gene_node_end }); const record1 = new Record( { @@ -559,12 +559,12 @@ describe('Testing QueryResults Module', () => { }); describe('query graph: gene1-disease1-gene2 (gene2 has ids param)', () => { - const gene_node_start = new QNode('n1', { categories: ['Gene'] }); - const disease_node = new QNode('n2', { categories: ['Disease'] }); - const gene_node_end = new QNode('n3', { categories: ['Gene'], ids: ['NCBIGene:7289'] }); + const gene_node_start = new QNode({ id: 'n1', categories: ['Gene'] }) ; + const disease_node = new QNode({ id: 'n2', categories: ['Disease'] }) ; + const gene_node_end = new QNode({ id: 'n3', categories: ['Gene'], ids: ['NCBIGene:7289'] }) ; - const edge1 = new QEdge('e01', { subject: gene_node_start, object: disease_node }); - const edge2 = new QEdge('e02', { subject: disease_node, object: gene_node_end }); + const edge1 = new QEdge({ id: 'e01', subject: gene_node_start, object: disease_node }); + const edge2 = new QEdge({ id: 'e02', subject: disease_node, object: gene_node_end }); const record1 = new Record( { @@ -682,12 +682,12 @@ describe('Testing QueryResults Module', () => { }); describe('Three Records', () => { - const gene_node_start = new QNode('n1', { categories: ['Gene'], ids: ['NCBIGene:3778'] }); - const disease_node = new QNode('n2', { categories: ['Disease'] }); - const gene_node_end = new QNode('n3', { categories: ['Gene'] }); + const gene_node_start = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:3778'] }) ; + const disease_node = new QNode({ id: 'n2', categories: ['Disease'] }) ; + const gene_node_end = new QNode({ id: 'n3', categories: ['Gene'] }) ; - const edge1 = new QEdge('e01', { subject: gene_node_start, object: disease_node }); - const edge2 = new QEdge('e02', { subject: disease_node, object: gene_node_end }); + const edge1 = new QEdge({ id: 'e01', subject: gene_node_start, object: disease_node }); + const edge2 = new QEdge({ id: 'e02', subject: disease_node, object: gene_node_end }); const record1 = new Record( { diff --git a/__test__/unittest/QueryEdge.test.js b/__test__/unittest/QueryEdge.test.js index 5f5c184f..0c54d85b 100644 --- a/__test__/unittest/QueryEdge.test.js +++ b/__test__/unittest/QueryEdge.test.js @@ -5,7 +5,8 @@ describe("Test QEdge class", () => { describe("Test getPredicate function", () => { test("Non reversed edge should return predicates itself", () => { - const edge = new qEdge('e01', { + const edge = new qEdge({ + id: 'e01', predicates: 'biolink:treats', object: { getCurie() { @@ -30,7 +31,8 @@ describe("Test QEdge class", () => { }) test("An array of non-undefined predicates should return itself", () => { - const edge = new qEdge('e01', { + const edge = new qEdge({ + id: 'e01', predicates: ['biolink:treats', 'biolink:targets'], object: { getCurie() { @@ -50,7 +52,8 @@ describe("Test QEdge class", () => { test("An array of non-undefined predicates with reverse edge should exclude return value if undefined", () => { - const edge = new qEdge('e01', { + const edge = new qEdge({ + id: 'e01', predicates: ['biolink:treats', 'biolink:targets'], object: { getCurie() { @@ -68,7 +71,8 @@ describe("Test QEdge class", () => { }) test("An array of non-undefined predicates with reverse edge should return reversed predicates if not undefined", () => { - const edge = new qEdge('e01', { + const edge = new qEdge({ + id: 'e01', predicates: ['biolink:treats', 'biolink:targets'], object: { getCurie() { @@ -89,7 +93,8 @@ describe("Test QEdge class", () => { describe("Test getOutputNode function", () => { test("reversed edge should return the subject", () => { - const edge = new qEdge('e01', { + const edge = new qEdge({ + id: 'e01', predicates: ['biolink:treats', 'biolink:targets'], object: { getCurie() { @@ -113,7 +118,8 @@ describe("Test QEdge class", () => { }) test("non reversed edge should return the object", () => { - const edge = new qEdge('e01', { + const edge = new qEdge({ + id: 'e01', predicates: ['biolink:treats', 'biolink:targets'], object: { getCurie() { diff --git a/src/query_edge.js b/src/query_edge.js index a13c6890..09074f3a 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -8,17 +8,17 @@ const QNode = require('./query_node'); module.exports = class QEdge { /** * - * @param {string} id - QEdge ID - * @param {object} info - QEdge info, e.g. subject, object, predicate + * @param {object} info - QEdge info, e.g. ID, subject, object, predicate * @param {boolean} reverse - is QEdge reversed? */ - constructor(id, info, reverse = null) { - this.id = id; + constructor(info, reverse = null) { + this.id = info.id; this.predicate = info.predicates; - this.subject = info.frozen === true ? QNode.unfreeze(info.subject) : info.subject; - this.object = info.frozen === true ? QNode.unfreeze(info.object) : info.object; + this.subject = info.frozen === true ? new QNode(info.subject) : info.subject; + this.object = info.frozen === true ? new QNode(info.object) : info.object; this.expanded_predicates = []; + if (info.reverse !== undefined) reverse = info.reverse; if (reverse === null) reverse = !(info.subject?.getCurie()) && !!(info.object?.getCurie()); this.init(); @@ -29,8 +29,11 @@ module.exports = class QEdge { this.executed = info.executed === undefined ? false : info.executed; //run initial checks this.logs = info.logs === undefined ? [] : info.logs; + //this edges query response records - this.records = []; + if (info.records && info.frozen === true) this.records = info.records.map(recordJSON => new Record(recordJSON)); + else this.records = []; + debug(`(2) Created Edge` + ` ${JSON.stringify(this.getID())} Reverse = ${this.reverse}`) } @@ -46,16 +49,11 @@ module.exports = class QEdge { subject: this.subject.freeze(), object: this.object.freeze(), predicate: this.predicate, - records: this.records.map(record => record.freeze()) + records: this.records.map(record => record.freeze()), + frozen: true }; } - static unfreeze(json) { - var output = new MegaQEdge(json.id, { ...json, frozen: true }, json.reverse === undefined ? false : json.reverse); - output.records = json.records.map(recordJSON => new Record(recordJSON)); - return output; - } - init() { this.expanded_predicates = this.getPredicate(); } diff --git a/src/query_graph.js b/src/query_graph.js index 9cc71d16..38e84157 100644 --- a/src/query_graph.js +++ b/src/query_graph.js @@ -258,10 +258,10 @@ module.exports = class QueryGraphHandler { ).getLog(), ); } - nodes[qNodeID] = new QNode(qNodeID, this.queryGraph.nodes[qNodeID]); + nodes[qNodeID] = new QNode({id: qNodeID, ...this.queryGraph.nodes[qNodeID]}); } else { debug(`Creating node...`); - nodes[qNodeID] = new QNode(qNodeID, this.queryGraph.nodes[qNodeID]); + nodes[qNodeID] = new QNode({id: qNodeID, ...this.queryGraph.nodes[qNodeID]}); } if (nodes[qNodeID].category !== undefined) { @@ -305,7 +305,7 @@ module.exports = class QueryGraphHandler { this.nodes[this.queryGraph.edges[qEdgeID].subject].updateConnection(qEdgeID); this.nodes[this.queryGraph.edges[qEdgeID].object].updateConnection(qEdgeID); - edges[edge_index] = [new QEdge(qEdgeID, edge_info)]; + edges[edge_index] = [new QEdge({id: qEdgeID, ...edge_info})]; edge_index++; } this.logs.push( diff --git a/src/query_node.js b/src/query_node.js index eeedf042..f897b343 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -7,11 +7,10 @@ const InvalidQueryGraphError = require('./exceptions/invalid_query_graph_error') module.exports = class QNode { /** * - * @param {string} id - QNode ID - * @param {object} info - Qnode info, e.g. curie, category + * @param {object} info - Qnode info, e.g. ID, curie, category */ - constructor(id, info) { - this.id = id; + constructor(info) { + this.id = info.id; this.category = info.categories || 'NamedThing'; // mainIDs this.curie = info.ids; @@ -50,11 +49,6 @@ module.exports = class QNode { } } - static unfreeze(json) { - var node = new QNode(json.id, json); - return node; - } - isSet() { //query node specified as set return this.is_set ? true : false; From c629e087a09cae571a12a04d45726635196e8b8f Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Fri, 21 Oct 2022 12:51:42 -0400 Subject: [PATCH 12/34] fix: allow qualifier_constraints --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index f891c28d..b0d7d9e7 100644 --- a/src/index.js +++ b/src/index.js @@ -262,7 +262,7 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { Object.values(item).forEach((element) => { element.constraints?.forEach((constraint) => constraints.add(constraint.name)); element.attribute_constraints?.forEach((constraint) => constraints.add(constraint.name)); - element.qualifier_constraints?.forEach((constraint) => constraints.add(constraint.name)); + // element.qualifier_constraints?.forEach((constraint) => constraints.add(constraint.name)); }); }); if (constraints.size) { From c475e0aff84565254da8a10a75de5ef418690358 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Fri, 21 Oct 2022 14:55:01 -0400 Subject: [PATCH 13/34] style: formatting --- .../integration/QEdge2BTEEdgeHandler.test.js | 122 ++-- __test__/integration/QueryEdge.test.js | 312 +++++----- .../integration/QueryGraphHandler.test.js | 540 +++++++++--------- __test__/integration/QueryNode.test.js | 300 +++++----- __test__/unittest/QueryEdge.test.js | 271 +++++---- 5 files changed, 775 insertions(+), 770 deletions(-) diff --git a/__test__/integration/QEdge2BTEEdgeHandler.test.js b/__test__/integration/QEdge2BTEEdgeHandler.test.js index 99cdd2b5..7cbd01ce 100644 --- a/__test__/integration/QEdge2BTEEdgeHandler.test.js +++ b/__test__/integration/QEdge2BTEEdgeHandler.test.js @@ -1,68 +1,70 @@ -const QNode = require("../../src/query_node"); -const QEdge = require("../../src/query_edge"); -const NodeUpdateHandler = require("../../src/update_nodes"); +const QNode = require('../../src/query_node'); +const QEdge = require('../../src/query_edge'); +const NodeUpdateHandler = require('../../src/update_nodes'); -describe("Testing NodeUpdateHandler Module", () => { - const gene_node1 = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; - const node1_equivalent_ids = { - "NCBIGene:1017": { - db_ids: { - NCBIGene: ["1017"], - SYMBOL: ['CDK2'] - } - } - } +describe('Testing NodeUpdateHandler Module', () => { + const gene_node1 = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:1017'] }); + const node1_equivalent_ids = { + 'NCBIGene:1017': { + db_ids: { + NCBIGene: ['1017'], + SYMBOL: ['CDK2'], + }, + }, + }; - const gene_node2 = new QNode({ id: "n2", categories: ["Gene"], ids: ["NCBIGene:1017", "NCBIGene:1018"] }) ; - const gene_node1_with_id_annotated = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; - gene_node1_with_id_annotated.setEquivalentIDs(node1_equivalent_ids); - //gene_node2.setEquivalentIDs(node2_equivalent_ids); - const chemical_node1 = new QNode({ id: "n3", categories:[ "SmallMolecule"] }) ; - const edge1 = new QEdge({ id: "e01", subject: gene_node1, object: chemical_node1 }); - const edge2 = new QEdge({ id: "e02", subject: gene_node1_with_id_annotated, object: chemical_node1 }); - const edge3 = new QEdge({ id: 'e04', subject: gene_node2, object: chemical_node1 }); - const edge4 = new QEdge({ id: 'e05', object: gene_node2, subject: chemical_node1 }); + const gene_node2 = new QNode({ id: 'n2', categories: ['Gene'], ids: ['NCBIGene:1017', 'NCBIGene:1018'] }); + const gene_node1_with_id_annotated = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:1017'] }); + gene_node1_with_id_annotated.setEquivalentIDs(node1_equivalent_ids); + //gene_node2.setEquivalentIDs(node2_equivalent_ids); + const chemical_node1 = new QNode({ id: 'n3', categories: ['SmallMolecule'] }); + const edge1 = new QEdge({ id: 'e01', subject: gene_node1, object: chemical_node1 }); + const edge2 = new QEdge({ id: 'e02', subject: gene_node1_with_id_annotated, object: chemical_node1 }); + const edge3 = new QEdge({ id: 'e04', subject: gene_node2, object: chemical_node1 }); + const edge4 = new QEdge({ id: 'e05', object: gene_node2, subject: chemical_node1 }); - describe("Testing _getCuries function", () => { - test("test edge with one curie input return an array of one", () => { - const nodeUpdater = new NodeUpdateHandler([edge1]); - const res = nodeUpdater._getCuries([edge1]); - expect(res).toHaveProperty("Gene", ["NCBIGene:1017"]); - }) + describe('Testing _getCuries function', () => { + test('test edge with one curie input return an array of one', () => { + const nodeUpdater = new NodeUpdateHandler([edge1]); + const res = nodeUpdater._getCuries([edge1]); + expect(res).toHaveProperty('Gene', ['NCBIGene:1017']); + }); - test("test edge with multiple curie input return an array with multiple items", () => { - const nodeUpdater = new NodeUpdateHandler([edge3]); - const res = nodeUpdater._getCuries([edge3]); - expect(res.Gene.length).toEqual(2); - }) + test('test edge with multiple curie input return an array with multiple items', () => { + const nodeUpdater = new NodeUpdateHandler([edge3]); + const res = nodeUpdater._getCuries([edge3]); + expect(res.Gene.length).toEqual(2); + }); - test("test edge with input node annotated should return an empty array", () => { - const nodeUpdater = new NodeUpdateHandler([edge2]); - const res = nodeUpdater._getCuries([edge2]); - expect(res).toEqual({}); - }) + test('test edge with input node annotated should return an empty array', () => { + const nodeUpdater = new NodeUpdateHandler([edge2]); + const res = nodeUpdater._getCuries([edge2]); + expect(res).toEqual({}); + }); - test("test edge with input on object end should be handled", () => { - const nodeUpdater = new NodeUpdateHandler([edge4]); - const res = nodeUpdater._getCuries([edge4]); - expect(res.Gene.length).toEqual(2); - }) - }) + test('test edge with input on object end should be handled', () => { + const nodeUpdater = new NodeUpdateHandler([edge4]); + const res = nodeUpdater._getCuries([edge4]); + expect(res.Gene.length).toEqual(2); + }); + }); - describe("Testing _getEquivalentIDs function", () => { - test("test edge with one curie input return an object with one key", async () => { - const nodeUpdater = new NodeUpdateHandler([edge1]); - const res = await nodeUpdater._getEquivalentIDs({ "Gene": ["NCBIGene:1017"] }) - expect(res).toHaveProperty("NCBIGene:1017"); - }) + describe('Testing _getEquivalentIDs function', () => { + test('test edge with one curie input return an object with one key', async () => { + const nodeUpdater = new NodeUpdateHandler([edge1]); + const res = await nodeUpdater._getEquivalentIDs({ Gene: ['NCBIGene:1017'] }); + expect(res).toHaveProperty('NCBIGene:1017'); + }); - test("test edge with multiple curie input return an object with multiple key", async () => { - const nodeUpdater = new NodeUpdateHandler([edge1]); - const res = await nodeUpdater._getEquivalentIDs({ "Gene": ["NCBIGene:1017", "NCBIGene:1018"], "SmallMolecule": ["PUBCHEM:5070"] }) - expect(res).toHaveProperty("NCBIGene:1017"); - expect(res).toHaveProperty("NCBIGene:1018"); - expect(res).toHaveProperty("PUBCHEM:5070"); - }) - }) - -}) \ No newline at end of file + test('test edge with multiple curie input return an object with multiple key', async () => { + const nodeUpdater = new NodeUpdateHandler([edge1]); + const res = await nodeUpdater._getEquivalentIDs({ + Gene: ['NCBIGene:1017', 'NCBIGene:1018'], + SmallMolecule: ['PUBCHEM:5070'], + }); + expect(res).toHaveProperty('NCBIGene:1017'); + expect(res).toHaveProperty('NCBIGene:1018'); + expect(res).toHaveProperty('PUBCHEM:5070'); + }); + }); +}); diff --git a/__test__/integration/QueryEdge.test.js b/__test__/integration/QueryEdge.test.js index 84a98147..dac4d0a7 100644 --- a/__test__/integration/QueryEdge.test.js +++ b/__test__/integration/QueryEdge.test.js @@ -1,151 +1,161 @@ -const QNode = require("../../src/query_node"); -const QEdge = require("../../src/query_edge"); - -describe("Testing QueryEdge Module", () => { - const gene_node1 = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; - const type_node = new QNode({ id: "n2", categories: ["SmallMolecule"] }) ; - const disease1_node = new QNode({ id: "n1", categories: ["Disease"], ids: ["MONDO:000123"] }) ; - const node1_equivalent_ids = { - "NCBIGene:1017": { - db_ids: { - NCBIGene: ["1017"], - SYMBOL: ['CDK2'] - } - } - } - - const gene_node2 = new QNode({ id: "n2", categories: "Gene", ids: ["NCBIGene:1017", "NCBIGene:1018"] }) ; - const gene_node1_with_id_annotated = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; - gene_node1_with_id_annotated.setEquivalentIDs(node1_equivalent_ids); - const chemical_node1 = new QNode({ id: "n3", categories: ["SmallMolecule"] }) ; - const edge1 = new QEdge({ id: "e01", subject: gene_node1, object: chemical_node1 }); - const edge2 = new QEdge({ id: "e02", subject: gene_node1_with_id_annotated, object: chemical_node1 }); - const edge3 = new QEdge({ id: 'e04', subject: gene_node2, object: chemical_node1 }); - const edge4 = new QEdge({ id: 'e05', object: gene_node2, subject: chemical_node1 }); - const edge5 = new QEdge({ id: 'e06', object: gene_node1_with_id_annotated, subject: chemical_node1 }); - - describe("Testing isReversed function", () => { - test("test if only the object of the edge has curie defined, should return true", () => { - const res = edge4.isReversed(); - expect(res).toBeTruthy(); - }); - - test("test if the subject of the edge has curie defined, should return false", () => { - const res = edge1.isReversed(); - expect(res).toBeFalsy(); - }); - - test("test if both subject and object curie not defined, should return false", () => { - const node1 = new QNode({ id: "n1", categories: ["Gene"] }) ; - const node2 = new QNode({ id: "n2", categories: ["SmallMolecule"] }) ; - const edge = new QEdge({ id: "e01", subject: node1, object: node2 }); - expect(edge.isReversed()).toBeFalsy(); - }); - - }) - - describe("Testing getInputCurie function", () => { - test("test return an array of one curie if subject has only one curie specified", () => { - const res = edge1.getInputCurie(); - expect(res).toEqual(['NCBIGene:1017']); - }); - - test("test return an array of two curie if subject has only an array of two curies specified", () => { - const res = edge3.getInputCurie(); - expect(res).toEqual(['NCBIGene:1017', 'NCBIGene:1018']); - }); - - test("test return an array of two curies if edge is reversed and object has two curies specified", () => { - const res = edge4.getInputCurie(); - expect(res).toEqual(['NCBIGene:1017', 'NCBIGene:1018']); - }); - - }) - - describe("Testing hasInput function", () => { - test("test return true if subject has only one curie specified", () => { - const res = edge1.hasInput(); - expect(res).toBeTruthy(); - }); - - test("test return true if subject has only an array of two curies specified", () => { - const res = edge3.hasInput(); - expect(res).toBeTruthy(); - }); - - test("test return true if subject has no curies specified but object does", () => { - const res = edge4.hasInput(); - expect(res).toBeTruthy(); - }); - - test("test return false if both subject and object has no curies specified", () => { - const node1 = new QNode({ id: "n1", categories: ["Gene"] }) ; - const node2 = new QNode({ id: "n2", categories: ["SmallMolecule"] }) ; - const edge = new QEdge({ id: "e01", subject: node1, object: node2 }); - expect(edge.hasInput()).toBeFalsy(); - }); - - }) - - - // Removed because new QEdge has different implementation for hasInputResolved - // describe("Testing hasInputResolved function", () => { - // test("test return true if subject has input resolved", () => { - // const res = edge2.hasInputResolved(); - // expect(res).toBeTruthy(); - // }); - - // test("test return false if both subject and object do not have input resolved", () => { - // const res = edge1.hasInputResolved(); - // expect(res).toBeFalsy(); - // }); - - // test("test return true if subject doesn't have input resolved, but object does", () => { - // const res = edge5.hasInputResolved(); - // expect(res).toBeTruthy(); - // }); - - // }) - - describe("Testing getPredicate function", () => { - test("test get reverse predicate if query is reversed", () => { - const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: ["biolink:treats"] }); - const res = edge.getPredicate(); - expect(res).toContain("treated_by"); - }); - - test("test get reverse predicate if query is reversed and expanded", () => { - const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: ["biolink:affects"] }); - const res = edge.getPredicate(); - expect(res).toContain("affected_by"); - expect(res).toContain("disrupted_by"); - }); - }) - - describe("Testing expandPredicates function", () => { - test("All predicates are correctly expanded if in biolink model", () => { - const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: ["biolink:contributes_to"] }); - const res = edge.expandPredicates(["contributes_to"]) - expect(res).toContain("contributes_to"); - expect(res).toContain("causes"); - }); - - test("Multiple predicates can be resolved", () => { - const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: ["biolink:contributes_to"] }); - const res = edge.expandPredicates(["contributes_to", "ameliorates"]) - expect(res).toContain("contributes_to"); - expect(res).toContain("causes"); - expect(res).toContain("ameliorates"); - expect(res).toContain("treats"); - }); - - test("Predicates not in biolink model should return itself", () => { - const edge = new QEdge({ id: "e01", subject: type_node, object: disease1_node, predicates: "biolink:contributes_to" }); - const res = edge.expandPredicates(["contributes_to", "amelio"]) - expect(res).toContain("contributes_to"); - expect(res).toContain("causes"); - expect(res).toContain("amelio"); - }); - }) - -}) \ No newline at end of file +const QNode = require('../../src/query_node'); +const QEdge = require('../../src/query_edge'); + +describe('Testing QueryEdge Module', () => { + const gene_node1 = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:1017'] }); + const type_node = new QNode({ id: 'n2', categories: ['SmallMolecule'] }); + const disease1_node = new QNode({ id: 'n1', categories: ['Disease'], ids: ['MONDO:000123'] }); + const node1_equivalent_ids = { + 'NCBIGene:1017': { + db_ids: { + NCBIGene: ['1017'], + SYMBOL: ['CDK2'], + }, + }, + }; + + const gene_node2 = new QNode({ id: 'n2', categories: 'Gene', ids: ['NCBIGene:1017', 'NCBIGene:1018'] }); + const gene_node1_with_id_annotated = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:1017'] }); + gene_node1_with_id_annotated.setEquivalentIDs(node1_equivalent_ids); + const chemical_node1 = new QNode({ id: 'n3', categories: ['SmallMolecule'] }); + const edge1 = new QEdge({ id: 'e01', subject: gene_node1, object: chemical_node1 }); + const edge2 = new QEdge({ id: 'e02', subject: gene_node1_with_id_annotated, object: chemical_node1 }); + const edge3 = new QEdge({ id: 'e04', subject: gene_node2, object: chemical_node1 }); + const edge4 = new QEdge({ id: 'e05', object: gene_node2, subject: chemical_node1 }); + const edge5 = new QEdge({ id: 'e06', object: gene_node1_with_id_annotated, subject: chemical_node1 }); + + describe('Testing isReversed function', () => { + test('test if only the object of the edge has curie defined, should return true', () => { + const res = edge4.isReversed(); + expect(res).toBeTruthy(); + }); + + test('test if the subject of the edge has curie defined, should return false', () => { + const res = edge1.isReversed(); + expect(res).toBeFalsy(); + }); + + test('test if both subject and object curie not defined, should return false', () => { + const node1 = new QNode({ id: 'n1', categories: ['Gene'] }); + const node2 = new QNode({ id: 'n2', categories: ['SmallMolecule'] }); + const edge = new QEdge({ id: 'e01', subject: node1, object: node2 }); + expect(edge.isReversed()).toBeFalsy(); + }); + }); + + describe('Testing getInputCurie function', () => { + test('test return an array of one curie if subject has only one curie specified', () => { + const res = edge1.getInputCurie(); + expect(res).toEqual(['NCBIGene:1017']); + }); + + test('test return an array of two curie if subject has only an array of two curies specified', () => { + const res = edge3.getInputCurie(); + expect(res).toEqual(['NCBIGene:1017', 'NCBIGene:1018']); + }); + + test('test return an array of two curies if edge is reversed and object has two curies specified', () => { + const res = edge4.getInputCurie(); + expect(res).toEqual(['NCBIGene:1017', 'NCBIGene:1018']); + }); + }); + + describe('Testing hasInput function', () => { + test('test return true if subject has only one curie specified', () => { + const res = edge1.hasInput(); + expect(res).toBeTruthy(); + }); + + test('test return true if subject has only an array of two curies specified', () => { + const res = edge3.hasInput(); + expect(res).toBeTruthy(); + }); + + test('test return true if subject has no curies specified but object does', () => { + const res = edge4.hasInput(); + expect(res).toBeTruthy(); + }); + + test('test return false if both subject and object has no curies specified', () => { + const node1 = new QNode({ id: 'n1', categories: ['Gene'] }); + const node2 = new QNode({ id: 'n2', categories: ['SmallMolecule'] }); + const edge = new QEdge({ id: 'e01', subject: node1, object: node2 }); + expect(edge.hasInput()).toBeFalsy(); + }); + }); + + // Removed because new QEdge has different implementation for hasInputResolved + // describe("Testing hasInputResolved function", () => { + // test("test return true if subject has input resolved", () => { + // const res = edge2.hasInputResolved(); + // expect(res).toBeTruthy(); + // }); + + // test("test return false if both subject and object do not have input resolved", () => { + // const res = edge1.hasInputResolved(); + // expect(res).toBeFalsy(); + // }); + + // test("test return true if subject doesn't have input resolved, but object does", () => { + // const res = edge5.hasInputResolved(); + // expect(res).toBeTruthy(); + // }); + + // }) + + describe('Testing getPredicate function', () => { + test('test get reverse predicate if query is reversed', () => { + const edge = new QEdge({ id: 'e01', subject: type_node, object: disease1_node, predicates: ['biolink:treats'] }); + const res = edge.getPredicate(); + expect(res).toContain('treated_by'); + }); + + test('test get reverse predicate if query is reversed and expanded', () => { + const edge = new QEdge({ id: 'e01', subject: type_node, object: disease1_node, predicates: ['biolink:affects'] }); + const res = edge.getPredicate(); + expect(res).toContain('affected_by'); + expect(res).toContain('disrupted_by'); + }); + }); + + describe('Testing expandPredicates function', () => { + test('All predicates are correctly expanded if in biolink model', () => { + const edge = new QEdge({ + id: 'e01', + subject: type_node, + object: disease1_node, + predicates: ['biolink:contributes_to'], + }); + const res = edge.expandPredicates(['contributes_to']); + expect(res).toContain('contributes_to'); + expect(res).toContain('causes'); + }); + + test('Multiple predicates can be resolved', () => { + const edge = new QEdge({ + id: 'e01', + subject: type_node, + object: disease1_node, + predicates: ['biolink:contributes_to'], + }); + const res = edge.expandPredicates(['contributes_to', 'ameliorates']); + expect(res).toContain('contributes_to'); + expect(res).toContain('causes'); + expect(res).toContain('ameliorates'); + expect(res).toContain('treats'); + }); + + test('Predicates not in biolink model should return itself', () => { + const edge = new QEdge({ + id: 'e01', + subject: type_node, + object: disease1_node, + predicates: 'biolink:contributes_to', + }); + const res = edge.expandPredicates(['contributes_to', 'amelio']); + expect(res).toContain('contributes_to'); + expect(res).toContain('causes'); + expect(res).toContain('amelio'); + }); + }); +}); diff --git a/__test__/integration/QueryGraphHandler.test.js b/__test__/integration/QueryGraphHandler.test.js index 4c9596db..2733925e 100644 --- a/__test__/integration/QueryGraphHandler.test.js +++ b/__test__/integration/QueryGraphHandler.test.js @@ -1,286 +1,284 @@ -const QueryGraphHandler = require("../../src/query_graph"); -const QNode2 = require("../../src/query_node"); -const QEdge = require("../../src/query_edge"); -const InvalidQueryGraphError = require("../../src/exceptions/invalid_query_graph_error"); +const QueryGraphHandler = require('../../src/query_graph'); +const QNode2 = require('../../src/query_node'); +const QEdge = require('../../src/query_edge'); +const InvalidQueryGraphError = require('../../src/exceptions/invalid_query_graph_error'); -describe("Testing QueryGraphHandler Module", () => { - const disease_entity_node = { - categories: ["biolink:Disease"], - ids: ["MONDO:0005737"] - }; - const gene_entity_node = { - categories: ["biolink:Gene"], - ids: ["NCBIGene:1017"] - }; - const gene_class_node = { - categories: ["biolink:Gene"] - }; - const chemical_class_node = { - categories: ["biolink:SmallMolecule"] - }; - const pathway_class_node = { - categories: ["biolink:Pathways"] - }; - const phenotype_class_node = { - categories: ["biolink:Phenotype"] - }; - const OneHopQuery = { - nodes: { - n0: disease_entity_node, - n1: gene_class_node - }, - edges: { - e01: { - subject: "n0", - object: "n1" - } - } - }; +describe('Testing QueryGraphHandler Module', () => { + const disease_entity_node = { + categories: ['biolink:Disease'], + ids: ['MONDO:0005737'], + }; + const gene_entity_node = { + categories: ['biolink:Gene'], + ids: ['NCBIGene:1017'], + }; + const gene_class_node = { + categories: ['biolink:Gene'], + }; + const chemical_class_node = { + categories: ['biolink:SmallMolecule'], + }; + const pathway_class_node = { + categories: ['biolink:Pathways'], + }; + const phenotype_class_node = { + categories: ['biolink:Phenotype'], + }; + const OneHopQuery = { + nodes: { + n0: disease_entity_node, + n1: gene_class_node, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + }, + }, + }; - const ThreeHopExplainQuery = { - nodes: { - n0: disease_entity_node, - n1: gene_class_node, - n2: chemical_class_node, - n3: gene_entity_node, - }, - edges: { - e01: { - subject: "n0", - object: "n1" - }, - e02: { - subject: "n1", - object: "n2" - }, - e03: { - subject: "n2", - object: "n3" - } - } - }; + const ThreeHopExplainQuery = { + nodes: { + n0: disease_entity_node, + n1: gene_class_node, + n2: chemical_class_node, + n3: gene_entity_node, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + }, + e02: { + subject: 'n1', + object: 'n2', + }, + e03: { + subject: 'n2', + object: 'n3', + }, + }, + }; - const FourHopQuery = { - nodes: { - n0: disease_entity_node, - n1: gene_class_node, - n2: chemical_class_node, - n3: phenotype_class_node, - n4: pathway_class_node, - }, - edges: { - e01: { - subject: "n0", - object: "n1" - }, - e02: { - subject: "n1", - object: "n2" - }, - e03: { - subject: "n2", - object: "n3" - }, - e04: { - subject: "n3", - object: "n4" - }, - } - }; + const FourHopQuery = { + nodes: { + n0: disease_entity_node, + n1: gene_class_node, + n2: chemical_class_node, + n3: phenotype_class_node, + n4: pathway_class_node, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + }, + e02: { + subject: 'n1', + object: 'n2', + }, + e03: { + subject: 'n2', + object: 'n3', + }, + e04: { + subject: 'n3', + object: 'n4', + }, + }, + }; - const QueryWithCycle1 = { - nodes: { - n0: disease_entity_node, - n1: gene_class_node, - n2: chemical_class_node, - n3: phenotype_class_node, - n4: pathway_class_node, - }, - edges: { - e01: { - subject: "n0", - object: "n1" - }, - e02: { - subject: "n1", - object: "n2" - }, - e03: { - subject: "n2", - object: "n3" - }, - e04: { - subject: "n3", - object: "n4" - }, - e05: { - subject: "n4", - object: "n1" - }, - } - }; + const QueryWithCycle1 = { + nodes: { + n0: disease_entity_node, + n1: gene_class_node, + n2: chemical_class_node, + n3: phenotype_class_node, + n4: pathway_class_node, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + }, + e02: { + subject: 'n1', + object: 'n2', + }, + e03: { + subject: 'n2', + object: 'n3', + }, + e04: { + subject: 'n3', + object: 'n4', + }, + e05: { + subject: 'n4', + object: 'n1', + }, + }, + }; - const QueryWithCycle2 = { - nodes: { - n0: disease_entity_node, - n1: gene_class_node, - n2: chemical_class_node, - n3: phenotype_class_node, - n4: pathway_class_node, - }, - edges: { - e01: { - subject: "n0", - object: "n1" - }, - e02: { - subject: "n1", - object: "n2" - }, - e03: { - subject: "n2", - object: "n3" - }, - e04: { - subject: "n3", - object: "n4" - }, - e05: { - subject: "n4", - object: "n1" - }, - } - }; + const QueryWithCycle2 = { + nodes: { + n0: disease_entity_node, + n1: gene_class_node, + n2: chemical_class_node, + n3: phenotype_class_node, + n4: pathway_class_node, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + }, + e02: { + subject: 'n1', + object: 'n2', + }, + e03: { + subject: 'n2', + object: 'n3', + }, + e04: { + subject: 'n3', + object: 'n4', + }, + e05: { + subject: 'n4', + object: 'n1', + }, + }, + }; - const QueryWithDuplicateEdge1 = { - nodes: { - n0: disease_entity_node, - n1: gene_class_node, - }, - edges: { - e01: { - subject: "n0", - object: "n1" - }, - e02: { - subject: "n1", - object: "n0" - } - } - }; + const QueryWithDuplicateEdge1 = { + nodes: { + n0: disease_entity_node, + n1: gene_class_node, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + }, + e02: { + subject: 'n1', + object: 'n0', + }, + }, + }; - const QueryWithNullValues = { - nodes: { - n0: { - ...disease_entity_node, - categories: null - }, - n1: { - ...gene_class_node, - ids: null - }, - }, - edges: { - e01: { - subject: "n0", - object: "n1" - } - } - }; + const QueryWithNullValues = { + nodes: { + n0: { + ...disease_entity_node, + categories: null, + }, + n1: { + ...gene_class_node, + ids: null, + }, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + }, + }, + }; - const QueryWithNullPredicate = { - nodes: { - n0: disease_entity_node, - n1: gene_class_node, - }, - edges: { - e01: { - subject: "n0", - object: "n1", - predicate: null - } - } - }; + const QueryWithNullPredicate = { + nodes: { + n0: disease_entity_node, + n1: gene_class_node, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + predicate: null, + }, + }, + }; - const QueryWithNullIds = { - nodes: { - n0: { - ...disease_entity_node, - ids: [] - }, - n1: { - ...gene_class_node, - ids: null - }, - }, - edges: { - e01: { - subject: "n0", - object: "n1" - } - } - }; + const QueryWithNullIds = { + nodes: { + n0: { + ...disease_entity_node, + ids: [], + }, + n1: { + ...gene_class_node, + ids: null, + }, + }, + edges: { + e01: { + subject: 'n0', + object: 'n1', + }, + }, + }; - describe("test _storeNodes function", () => { - - test("test if storeNodes with one hop query", async () => { - let handler = new QueryGraphHandler(OneHopQuery); - let nodes = await handler._storeNodes(); - expect(nodes).toHaveProperty("n0"); - expect(nodes).not.toHaveProperty("n2"); - expect(nodes.n0).toBeInstanceOf(QNode2); - }); - - test("test if storeNodes with multi hop query", async () => { - let handler = new QueryGraphHandler(FourHopQuery); - let nodes = await handler._storeNodes(); - expect(nodes).toHaveProperty("n0"); - expect(nodes).toHaveProperty("n3"); - expect(nodes.n0).toBeInstanceOf(QNode2); - expect(nodes.n3).toBeInstanceOf(QNode2); - }); + describe('test _storeNodes function', () => { + test('test if storeNodes with one hop query', async () => { + let handler = new QueryGraphHandler(OneHopQuery); + let nodes = await handler._storeNodes(); + expect(nodes).toHaveProperty('n0'); + expect(nodes).not.toHaveProperty('n2'); + expect(nodes.n0).toBeInstanceOf(QNode2); }); - describe("test _createQueryPaths function", () => { + test('test if storeNodes with multi hop query', async () => { + let handler = new QueryGraphHandler(FourHopQuery); + let nodes = await handler._storeNodes(); + expect(nodes).toHaveProperty('n0'); + expect(nodes).toHaveProperty('n3'); + expect(nodes.n0).toBeInstanceOf(QNode2); + expect(nodes.n3).toBeInstanceOf(QNode2); + }); + }); - test("test createQueryPaths with three hop explain query", async () => { - let handler = new QueryGraphHandler(ThreeHopExplainQuery); - let edges = await handler.calculateEdges(); - expect(Object.keys(edges)).toHaveLength(3); - expect(edges[0]).toHaveLength(1); - expect(edges[1]).toHaveLength(1); - expect(edges[2]).toHaveLength(1); - }); + describe('test _createQueryPaths function', () => { + test('test createQueryPaths with three hop explain query', async () => { + let handler = new QueryGraphHandler(ThreeHopExplainQuery); + let edges = await handler.calculateEdges(); + expect(Object.keys(edges)).toHaveLength(3); + expect(edges[0]).toHaveLength(1); + expect(edges[1]).toHaveLength(1); + expect(edges[2]).toHaveLength(1); + }); + }); + describe('test cycle/duplicate edge detection for query graphs', () => { + test('Duplicate Edge Graph #1', async () => { + const handler = new QueryGraphHandler(QueryWithDuplicateEdge1); + await expect(handler.calculateEdges()).rejects.toThrow(InvalidQueryGraphError); + }); + test('Query Graph Cycle #1', async () => { + const handler = new QueryGraphHandler(QueryWithCycle1); + await expect(handler.calculateEdges()).rejects.toThrow(InvalidQueryGraphError); }); - describe("test cycle/duplicate edge detection for query graphs", () => { - test("Duplicate Edge Graph #1", async () => { - const handler = new QueryGraphHandler(QueryWithDuplicateEdge1) - await expect(handler.calculateEdges()).rejects.toThrow(InvalidQueryGraphError) - }) - test("Query Graph Cycle #1", async () => { - const handler = new QueryGraphHandler(QueryWithCycle1) - await expect(handler.calculateEdges()).rejects.toThrow(InvalidQueryGraphError) - }) - test("Query Graph Cycle #2", async () => { - const handler = new QueryGraphHandler(QueryWithCycle2) - await expect(handler.calculateEdges()).rejects.toThrow(InvalidQueryGraphError) - }) - }) + test('Query Graph Cycle #2', async () => { + const handler = new QueryGraphHandler(QueryWithCycle2); + await expect(handler.calculateEdges()).rejects.toThrow(InvalidQueryGraphError); + }); + }); - describe("test chandling of null ids / categories / predicates", () => { - test("Null id/categories graph", async () => { - const handler = new QueryGraphHandler(QueryWithNullValues) - await expect(handler.calculateEdges()).resolves.not.toThrow() - }) - test("Null predicate graph", async () => { - const handler = new QueryGraphHandler(QueryWithNullPredicate) - const edges = await handler.calculateEdges() - // if this is undefined (not null) then smartapi-kg treats as if the field doesn't exist (desired behavior) - expect(edges[0][0].getPredicate()).toBe(undefined) - }) - test("Graph without any ids", async () => { - const handler = new QueryGraphHandler(QueryWithNullIds) - expect(handler.calculateEdges()).rejects.toThrow(InvalidQueryGraphError) - }) - }) -}); \ No newline at end of file + describe('test chandling of null ids / categories / predicates', () => { + test('Null id/categories graph', async () => { + const handler = new QueryGraphHandler(QueryWithNullValues); + await expect(handler.calculateEdges()).resolves.not.toThrow(); + }); + test('Null predicate graph', async () => { + const handler = new QueryGraphHandler(QueryWithNullPredicate); + const edges = await handler.calculateEdges(); + // if this is undefined (not null) then smartapi-kg treats as if the field doesn't exist (desired behavior) + expect(edges[0][0].getPredicate()).toBe(undefined); + }); + test('Graph without any ids', async () => { + const handler = new QueryGraphHandler(QueryWithNullIds); + expect(handler.calculateEdges()).rejects.toThrow(InvalidQueryGraphError); + }); + }); +}); diff --git a/__test__/integration/QueryNode.test.js b/__test__/integration/QueryNode.test.js index 1067822d..7ec4934b 100644 --- a/__test__/integration/QueryNode.test.js +++ b/__test__/integration/QueryNode.test.js @@ -1,165 +1,165 @@ -const QNode = require("../../src/query_node"); +const QNode = require('../../src/query_node'); -describe("Testing QueryNode Module", () => { - const node1_equivalent_ids = { - "NCBIGene:1017": { - db_ids: { - NCBIGene: ["1017"], - SYMBOL: ['CDK2'] - } - } - } +describe('Testing QueryNode Module', () => { + const node1_equivalent_ids = { + 'NCBIGene:1017': { + db_ids: { + NCBIGene: ['1017'], + SYMBOL: ['CDK2'], + }, + }, + }; - describe("Testing hasInput function", () => { - test("test node without curies specified should return false", () => { - const gene_node = new QNode({ id: "n1", categories: ["Gene"] }) ; - const res = gene_node.hasInput(); - expect(res).toBeFalsy(); - }) + describe('Testing hasInput function', () => { + test('test node without curies specified should return false', () => { + const gene_node = new QNode({ id: 'n1', categories: ['Gene'] }); + const res = gene_node.hasInput(); + expect(res).toBeFalsy(); + }); - test("test node with curies specified should return true", () => { - const gene_node = new QNode({ id: "n1", categories: ["Gene"], ids: ["NCBIGene:1017"] }) ; - const res = gene_node.hasInput(); - expect(res).toBeTruthy(); - }) - }) + test('test node with curies specified should return true', () => { + const gene_node = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:1017'] }); + const res = gene_node.hasInput(); + expect(res).toBeTruthy(); + }); + }); - describe("Test hasEquivalentIDs function", () => { - test("test node with equivalent identifiers set should return true", () => { - const gene_node = new QNode({ id: "n1", categories: ["Gene"] }) ; - gene_node.setEquivalentIDs(node1_equivalent_ids); - const res = gene_node.hasEquivalentIDs(); - expect(res).toBeTruthy(); - }); + describe('Test hasEquivalentIDs function', () => { + test('test node with equivalent identifiers set should return true', () => { + const gene_node = new QNode({ id: 'n1', categories: ['Gene'] }); + gene_node.setEquivalentIDs(node1_equivalent_ids); + const res = gene_node.hasEquivalentIDs(); + expect(res).toBeTruthy(); + }); - test("test node with equivalent identifiers not set should return false", () => { - const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; - const res = gene_node.hasEquivalentIDs(); - expect(res).toBeFalsy(); - }) - }) + test('test node with equivalent identifiers not set should return false', () => { + const gene_node = new QNode({ id: 'n1', categories: 'Gene' }); + const res = gene_node.hasEquivalentIDs(); + expect(res).toBeFalsy(); + }); + }); - describe("Test getEntities", () => { - test("If equivalent ids are empty, should return an empty array", () => { - const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; - gene_node.equivalentIDs = {}; - expect(gene_node.getEntities()).toEqual([]); - }) + describe('Test getEntities', () => { + test('If equivalent ids are empty, should return an empty array', () => { + const gene_node = new QNode({ id: 'n1', categories: 'Gene' }); + gene_node.equivalentIDs = {}; + expect(gene_node.getEntities()).toEqual([]); + }); - test("If equivalent ids are not empty, should return an array of bioentities", () => { - const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; - gene_node.equivalentIDs = { - "A": [ - { - "a": "b" - }, - { - "c": "d" - } - ], - "B": [ - { - "e": "f" - } - ] - }; - expect(gene_node.getEntities()).toEqual([ - { - "a": "b" - }, - { - "c": "d" - }, - { - "e": "f" - } - ]); - }) - }) + test('If equivalent ids are not empty, should return an array of bioentities', () => { + const gene_node = new QNode({ id: 'n1', categories: 'Gene' }); + gene_node.equivalentIDs = { + A: [ + { + a: 'b', + }, + { + c: 'd', + }, + ], + B: [ + { + e: 'f', + }, + ], + }; + expect(gene_node.getEntities()).toEqual([ + { + a: 'b', + }, + { + c: 'd', + }, + { + e: 'f', + }, + ]); + }); + }); - describe("Test getPrimaryIDs", () => { - test("If equivalent ids are empty, should return an empty array", () => { - const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; - gene_node.equivalentIDs = {}; - expect(gene_node.getPrimaryIDs()).toEqual([]); - }) + describe('Test getPrimaryIDs', () => { + test('If equivalent ids are empty, should return an empty array', () => { + const gene_node = new QNode({ id: 'n1', categories: 'Gene' }); + gene_node.equivalentIDs = {}; + expect(gene_node.getPrimaryIDs()).toEqual([]); + }); - test("If equivalent ids are not empty, should return an array of primaryIDs", () => { - const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; - gene_node.equivalentIDs = { - "A": [ - { - "primaryID": "b" - }, - { - "primaryID": "c" - } - ], - "B": [ - { - "primaryID": "d" - } - ] - }; - expect(gene_node.getPrimaryIDs()).toEqual(["b", "c", "d"]) - }) - }) + test('If equivalent ids are not empty, should return an array of primaryIDs', () => { + const gene_node = new QNode({ id: 'n1', categories: 'Gene' }); + gene_node.equivalentIDs = { + A: [ + { + primaryID: 'b', + }, + { + primaryID: 'c', + }, + ], + B: [ + { + primaryID: 'd', + }, + ], + }; + expect(gene_node.getPrimaryIDs()).toEqual(['b', 'c', 'd']); + }); + }); - describe("Test updateEquivalentIDs", () => { - test("If equivalent ids does not exist, should set it with the input", () => { - const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; - gene_node.updateEquivalentIDs({ "a": "b" }) - expect(gene_node.equivalentIDs).toEqual({ "a": "b" }); - }) + describe('Test updateEquivalentIDs', () => { + test('If equivalent ids does not exist, should set it with the input', () => { + const gene_node = new QNode({ id: 'n1', categories: 'Gene' }); + gene_node.updateEquivalentIDs({ a: 'b' }); + expect(gene_node.equivalentIDs).toEqual({ a: 'b' }); + }); - test("If equivalent ids are not empty, should update the equivalent ids", () => { - const gene_node = new QNode({ id: "n1", categories: "Gene" }) ; - gene_node.equivalentIDs = { "a": "b", "c": "d" }; - gene_node.updateEquivalentIDs({ "e": "f" }) - expect(gene_node.getEquivalentIDs()).toEqual({ "a": "b", "c": "d", "e": "f" }) - }) - }) + test('If equivalent ids are not empty, should update the equivalent ids', () => { + const gene_node = new QNode({ id: 'n1', categories: 'Gene' }); + gene_node.equivalentIDs = { a: 'b', c: 'd' }; + gene_node.updateEquivalentIDs({ e: 'f' }); + expect(gene_node.getEquivalentIDs()).toEqual({ a: 'b', c: 'd', e: 'f' }); + }); + }); - describe("Test getCategories function", () => { - test("If equivalent ids are empty, return itself and its descendants", () => { - const node = new QNode({ id: "n1", categories: "DiseaseOrPhenotypicFeature" }) ; - expect(node.getCategories()).toContain("Disease"); - expect(node.getCategories()).toContain("PhenotypicFeature"); - expect(node.getCategories()).toContain("DiseaseOrPhenotypicFeature"); - }) + describe('Test getCategories function', () => { + test('If equivalent ids are empty, return itself and its descendants', () => { + const node = new QNode({ id: 'n1', categories: 'DiseaseOrPhenotypicFeature' }); + expect(node.getCategories()).toContain('Disease'); + expect(node.getCategories()).toContain('PhenotypicFeature'); + expect(node.getCategories()).toContain('DiseaseOrPhenotypicFeature'); + }); - test("If equivalent ids are empty, return itself and its descendants using NamedThing as example", () => { - const node = new QNode({ id: "n1", categories: "NamedThing" }) ; - expect(node.getCategories()).toContain("Disease"); - expect(node.getCategories()).toContain("PhenotypicFeature"); - expect(node.getCategories()).toContain("DiseaseOrPhenotypicFeature"); - expect(node.getCategories()).toContain("Gene"); - expect(node.getCategories()).toContain("NamedThing"); - }) + test('If equivalent ids are empty, return itself and its descendants using NamedThing as example', () => { + const node = new QNode({ id: 'n1', categories: 'NamedThing' }); + expect(node.getCategories()).toContain('Disease'); + expect(node.getCategories()).toContain('PhenotypicFeature'); + expect(node.getCategories()).toContain('DiseaseOrPhenotypicFeature'); + expect(node.getCategories()).toContain('Gene'); + expect(node.getCategories()).toContain('NamedThing'); + }); - test("If equivalent ids are empty, return itself and its descendants using Gene as example", () => { - const node = new QNode({ id: "n1", categories: "Gene" }) ; - expect(node.getCategories()).toEqual(["Gene"]) - }) + test('If equivalent ids are empty, return itself and its descendants using Gene as example', () => { + const node = new QNode({ id: 'n1', categories: 'Gene' }); + expect(node.getCategories()).toEqual(['Gene']); + }); - test("If equivalent ids are not empty, return all semantic types defined in the entity", () => { - const node = new QNode({ id: "n1", categories: "Gene" }) ; - node.equivalentIDs = { - "A": [ - { - "semanticTypes": ["m", "n"] - }, - { - "semanticTypes": ["p", "q"] - } - ], - "B": [ - { - "semanticTypes": ["x", "y"] - } - ] - }; - expect(node.getCategories()).toEqual(["m", "n", "p", "q", "x", "y"]) - }) - }) -}) \ No newline at end of file + test('If equivalent ids are not empty, return all semantic types defined in the entity', () => { + const node = new QNode({ id: 'n1', categories: 'Gene' }); + node.equivalentIDs = { + A: [ + { + semanticTypes: ['m', 'n'], + }, + { + semanticTypes: ['p', 'q'], + }, + ], + B: [ + { + semanticTypes: ['x', 'y'], + }, + ], + }; + expect(node.getCategories()).toEqual(['m', 'n', 'p', 'q', 'x', 'y']); + }); + }); +}); diff --git a/__test__/unittest/QueryEdge.test.js b/__test__/unittest/QueryEdge.test.js index 0c54d85b..d76affbd 100644 --- a/__test__/unittest/QueryEdge.test.js +++ b/__test__/unittest/QueryEdge.test.js @@ -1,145 +1,140 @@ -const qEdge = require("../../src/query_edge"); +const qEdge = require('../../src/query_edge'); +describe('Test QEdge class', () => { + describe('Test getPredicate function', () => { + test('Non reversed edge should return predicates itself', () => { + const edge = new qEdge({ + id: 'e01', + predicates: 'biolink:treats', + object: { + getCurie() { + return undefined; + }, + }, + subject: { + getCurie() { + return 'uye'; + }, + }, + }); + const res = edge.getPredicate(); + expect(res).toContain('treats'); + }); -describe("Test QEdge class", () => { - describe("Test getPredicate function", () => { + test('Undefined predicate should return itself', () => { + const edge = new qEdge('e01', {}); + const res = edge.getPredicate(); + expect(res).toBeUndefined; + }); - test("Non reversed edge should return predicates itself", () => { - const edge = new qEdge({ - id: 'e01', - predicates: 'biolink:treats', - object: { - getCurie() { - return undefined; - } - }, - subject: { - getCurie() { - return 'uye' - } - } - }) - const res = edge.getPredicate(); - expect(res).toContain("treats"); - }) + test('An array of non-undefined predicates should return itself', () => { + const edge = new qEdge({ + id: 'e01', + predicates: ['biolink:treats', 'biolink:targets'], + object: { + getCurie() { + return undefined; + }, + }, + subject: { + getCurie() { + return 'yes'; + }, + }, + }); + const res = edge.getPredicate(); + expect(res).toContain('treats'); + expect(res).toContain('targets'); + }); - test("Undefined predicate should return itself", () => { - const edge = new qEdge('e01', { - }) - const res = edge.getPredicate(); - expect(res).toBeUndefined; - }) + test('An array of non-undefined predicates with reverse edge should exclude return value if undefined', () => { + const edge = new qEdge({ + id: 'e01', + predicates: ['biolink:treats', 'biolink:targets'], + object: { + getCurie() { + return 'yes'; + }, + }, + subject: { + getCurie() { + return undefined; + }, + }, + }); + const res = edge.getPredicate(); + expect(res).toContain('treated_by'); + }); - test("An array of non-undefined predicates should return itself", () => { - const edge = new qEdge({ - id: 'e01', - predicates: ['biolink:treats', 'biolink:targets'], - object: { - getCurie() { - return undefined - } - }, - subject: { - getCurie() { - return 'yes'; - } - } - }) - const res = edge.getPredicate(); - expect(res).toContain("treats"); - expect(res).toContain("targets"); - }) + test('An array of non-undefined predicates with reverse edge should return reversed predicates if not undefined', () => { + const edge = new qEdge({ + id: 'e01', + predicates: ['biolink:treats', 'biolink:targets'], + object: { + getCurie() { + return 'yes'; + }, + }, + subject: { + getCurie() { + return undefined; + }, + }, + }); + const res = edge.getPredicate(); + expect(res).toContain('treated_by'); + }); + }); - test("An array of non-undefined predicates with reverse edge should exclude return value if undefined", () => { + describe('Test getOutputNode function', () => { + test('reversed edge should return the subject', () => { + const edge = new qEdge({ + id: 'e01', + predicates: ['biolink:treats', 'biolink:targets'], + object: { + getCurie() { + return 'yes'; + }, + id() { + return 1; + }, + }, + subject: { + getCurie() { + return undefined; + }, + id() { + return 2; + }, + }, + }); + const res = edge.getOutputNode(); + expect(res.id()).toEqual(2); + }); - const edge = new qEdge({ - id: 'e01', - predicates: ['biolink:treats', 'biolink:targets'], - object: { - getCurie() { - return 'yes' - } - }, - subject: { - getCurie() { - return undefined; - } - } - }) - const res = edge.getPredicate(); - expect(res).toContain("treated_by") - }) - - test("An array of non-undefined predicates with reverse edge should return reversed predicates if not undefined", () => { - const edge = new qEdge({ - id: 'e01', - predicates: ['biolink:treats', 'biolink:targets'], - object: { - getCurie() { - return 'yes' - } - }, - subject: { - getCurie() { - return undefined; - } - } - }) - const res = edge.getPredicate(); - expect(res).toContain("treated_by"); - }) - }) - - describe("Test getOutputNode function", () => { - test("reversed edge should return the subject", () => { - - const edge = new qEdge({ - id: 'e01', - predicates: ['biolink:treats', 'biolink:targets'], - object: { - getCurie() { - return 'yes' - }, - id() { - return 1 - } - }, - subject: { - getCurie() { - return undefined; - }, - id() { - return 2 - } - } - }) - const res = edge.getOutputNode(); - expect(res.id()).toEqual(2); - }) - - test("non reversed edge should return the object", () => { - const edge = new qEdge({ - id: 'e01', - predicates: ['biolink:treats', 'biolink:targets'], - object: { - getCurie() { - return undefined - }, - id() { - return 1 - } - }, - subject: { - getCurie() { - return 'aa'; - }, - id() { - return 2 - } - } - }) - const res = edge.getOutputNode(); - expect(res.id()).toEqual(1); - }) - }) -}) \ No newline at end of file + test('non reversed edge should return the object', () => { + const edge = new qEdge({ + id: 'e01', + predicates: ['biolink:treats', 'biolink:targets'], + object: { + getCurie() { + return undefined; + }, + id() { + return 1; + }, + }, + subject: { + getCurie() { + return 'aa'; + }, + id() { + return 2; + }, + }, + }); + const res = edge.getOutputNode(); + expect(res.id()).toEqual(1); + }); + }); +}); From bf8413d3a1104c93ef340ab41e921137969b1fcb Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Mon, 31 Oct 2022 12:38:17 -0400 Subject: [PATCH 14/34] fix: resolve check, qXEdge -> qEdge, formatting --- src/batch_edge_query.js | 52 ++--- src/cache_handler.js | 56 ++--- src/edge_manager.js | 134 ++++++------ src/index.js | 94 ++++----- src/qedge2apiedge.js | 34 +-- src/query_edge.js | 153 +++++++------- src/query_node.js | 453 ++++++++++++++++++++-------------------- src/update_nodes.js | 36 ++-- 8 files changed, 506 insertions(+), 506 deletions(-) diff --git a/src/batch_edge_query.js b/src/batch_edge_query.js index 30389df9..dcc79437 100644 --- a/src/batch_edge_query.js +++ b/src/batch_edge_query.js @@ -20,17 +20,17 @@ module.exports = class BatchEdgeQueryHandler { } /** - * @param {Array} qXEdges - an array of TRAPI Query Edges; + * @param {Array} qEdges - an array of TRAPI Query Edges; */ - setEdges(qXEdges) { - this.qXEdges = qXEdges; + setEdges(qEdges) { + this.qEdges = qEdges; } /** * */ getEdges() { - return this.qXEdges; + return this.qEdges; } /** @@ -64,23 +64,26 @@ module.exports = class BatchEdgeQueryHandler { * Remove curies which resolve to the same thing, keeping the first. * @private */ - async _rmEquivalentDuplicates(qXEdges) { - Object.values(qXEdges).forEach((qXEdge) => { + async _rmEquivalentDuplicates(qEdges) { + Object.values(qEdges).forEach((qEdge) => { const nodes = { - subject: qXEdge.subject, - object: qXEdge.object, + subject: qEdge.subject, + object: qEdge.object, }; const strippedCuries = []; Object.entries(nodes).forEach(([nodeType, node]) => { const reducedCuries = []; const nodeStrippedCuries = []; - if (!node.curie) { return; } + if (!node.curie) { + return; + } node.curie.forEach((curie) => { // if the curie is already present, or an equivalent is, remove it if (!reducedCuries.includes(curie)) { - const equivalentAlreadyIncluded = qXEdge.getInputNode().getEquivalentIDs()[curie][0].curies.some( - (equivalentCurie) => reducedCuries.includes(equivalentCurie), - ); + const equivalentAlreadyIncluded = qEdge + .getInputNode() + .getEquivalentIDs() + [curie][0].curies.some((equivalentCurie) => reducedCuries.includes(equivalentCurie)); if (!equivalentAlreadyIncluded) { reducedCuries.push(curie); } else { @@ -99,34 +102,34 @@ module.exports = class BatchEdgeQueryHandler { } }); strippedCuries.forEach((curie) => { - qXEdge.getInputNode().removeEquivalentID(curie); + qEdge.getInputNode().removeEquivalentID(curie); }); }); } - async query(qXEdges, unavailableAPIs = {}) { + async query(qEdges, unavailableAPIs = {}) { debug('Node Update Start'); //it's now a single edge but convert to arr to simplify refactoring - qXEdges = Array.isArray(qXEdges) ? qXEdges : [qXEdges]; - const nodeUpdate = new NodesUpdateHandler(qXEdges); + qEdges = Array.isArray(qEdges) ? qEdges : [qEdges]; + const nodeUpdate = new NodesUpdateHandler(qEdges); //difference is there is no previous edge info anymore - await nodeUpdate.setEquivalentIDs(qXEdges); - await this._rmEquivalentDuplicates(qXEdges); + await nodeUpdate.setEquivalentIDs(qEdges); + await this._rmEquivalentDuplicates(qEdges); debug('Node Update Success'); const cacheHandler = new CacheHandler(this.caching, this.metaKG, this.recordConfig); - const { cachedRecords, nonCachedQXEdges } = await cacheHandler.categorizeEdges(qXEdges); + const { cachedRecords, nonCachedQEdges } = await cacheHandler.categorizeEdges(qEdges); this.logs = [...this.logs, ...cacheHandler.logs]; let queryRecords; - if (nonCachedQXEdges.length === 0) { + if (nonCachedQEdges.length === 0) { queryRecords = []; if (parentPort) { parentPort.postMessage({ cacheDone: true }); } } else { - debug('Start to convert qXEdges into APIEdges....'); - const edgeConverter = new QEdge2APIEdgeHandler(nonCachedQXEdges, this.metaKG); - const APIEdges = await edgeConverter.convert(nonCachedQXEdges); + debug('Start to convert qEdges into APIEdges....'); + const edgeConverter = new QEdge2APIEdgeHandler(nonCachedQEdges, this.metaKG); + const APIEdges = await edgeConverter.convert(nonCachedQEdges); debug(`qEdges are successfully converted into ${APIEdges.length} APIEdges....`); this.logs = [...this.logs, ...edgeConverter.logs]; if (APIEdges.length === 0 && cachedRecords.length === 0) { @@ -141,7 +144,8 @@ module.exports = class BatchEdgeQueryHandler { debug(`Total number of records is (${queryRecords.length})`); if (!isMainThread) { cacheHandler.cacheEdges(queryRecords); - } else { // await caching if async so end of job doesn't cut it off + } else { + // await caching if async so end of job doesn't cut it off await cacheHandler.cacheEdges(queryRecords); } } diff --git a/src/cache_handler.js b/src/cache_handler.js index 69917b87..9b042077 100644 --- a/src/cache_handler.js +++ b/src/cache_handler.js @@ -98,20 +98,20 @@ module.exports = class { ); } - async categorizeEdges(qXEdges) { + async categorizeEdges(qEdges) { if (this.cacheEnabled === false || process.env.INTERNAL_DISABLE_REDIS) { return { cachedRecords: [], - nonCachedQXEdges: qXEdges, + nonCachedQEdges: qEdges, }; } - let nonCachedQXEdges = []; + let nonCachedQEdges = []; let cachedRecords = []; debug('Begin edge cache lookup...'); - await async.eachSeries(qXEdges, async (qXEdge) => { - const qXEdgeMetaKGHash = this._hashEdgeByMetaKG(qXEdge.getHashedEdgeRepresentation()); + await async.eachSeries(qEdges, async (qEdge) => { + const qEdgeMetaKGHash = this._hashEdgeByMetaKG(qEdge.getHashedEdgeRepresentation()); const unpackedRecords = await new Promise(async (resolve) => { - const redisID = 'bte:edgeCache:' + qXEdgeMetaKGHash; + const redisID = 'bte:edgeCache:' + qEdgeMetaKGHash; await redisClient.client.usingLock([`redisLock:${redisID}`], 600000, async (signal) => { try { const compressedRecordPack = await redisClient.client.hgetallTimeout(redisID); @@ -129,7 +129,7 @@ module.exports = class { recordStream .pipe(this.createDecodeStream()) .on('data', (obj) => recordPack.push(obj)) - .on('end', () => resolve(Record.unpackRecords(recordPack, qXEdge, this.recordConfig))); + .on('end', () => resolve(Record.unpackRecords(recordPack, qEdge, this.recordConfig))); } else { resolve(null); } @@ -145,47 +145,47 @@ module.exports = class { new LogEntry( 'DEBUG', null, - `BTE finds cached records for ${qXEdge.getID()}`, + `BTE finds cached records for ${qEdge.getID()}`, { type: 'cacheHit', - qEdgeID: qXEdge.getID(), + qEdgeID: qEdge.getID(), } ).getLog() ); cachedRecords = [...cachedRecords, ...unpackedRecords]; } else { - nonCachedQXEdges.push(qXEdge); + nonCachedQEdges.push(qEdge); } debug(`Found (${cachedRecords.length}) cached records.`); }); - return { cachedRecords, nonCachedQXEdges }; + return { cachedRecords, nonCachedQEdges }; } - _hashEdgeByMetaKG(qXEdgeHash) { + _hashEdgeByMetaKG(qEdgeHash) { if (!this.metaKG) { - return qXEdgeHash; + return qEdgeHash; } const len = String(this.metaKG.ops.length); const allIDs = Array.from(new Set(this.metaKG.ops.map((op) => op.association.smartapi.id))).join(''); - return helper._generateHash(qXEdgeHash + len + allIDs); + return helper._generateHash(qEdgeHash + len + allIDs); } - _groupQueryRecordsByQXEdgeHash(queryRecords) { + _groupQueryRecordsByQEdgeHash(queryRecords) { let groupedRecords = {}; queryRecords.map((record) => { try { - const qXEdgeMetaKGHash = this._hashEdgeByMetaKG(record.qXEdge.getHashedEdgeRepresentation()); - if (!(qXEdgeMetaKGHash in groupedRecords)) { - groupedRecords[qXEdgeMetaKGHash] = []; + const qEdgeMetaKGHash = this._hashEdgeByMetaKG(record.qEdge.getHashedEdgeRepresentation()); + if (!(qEdgeMetaKGHash in groupedRecords)) { + groupedRecords[qEdgeMetaKGHash] = []; } - groupedRecords[qXEdgeMetaKGHash].push(record); + groupedRecords[qEdgeMetaKGHash].push(record); } catch (e) { debug('skipping malformed record'); } }); - Object.entries(groupedRecords).forEach(([qXEdgeMetaKGHash, records]) => { - groupedRecords[qXEdgeMetaKGHash] = Record.packRecords(records); + Object.entries(groupedRecords).forEach(([qEdgeMetaKGHash, records]) => { + groupedRecords[qEdgeMetaKGHash] = Record.packRecords(records); }); return groupedRecords; } @@ -210,11 +210,11 @@ module.exports = class { } debug('Start to cache query records.'); try { - const groupedRecords = this._groupQueryRecordsByQXEdgeHash(queryRecords); - const qXEdgeHashes = Array.from(Object.keys(groupedRecords)); - debug(`Number of hashed edges: ${qXEdgeHashes.length}`); + const groupedRecords = this._groupQueryRecordsByQEdgeHash(queryRecords); + const qEdgeHashes = Array.from(Object.keys(groupedRecords)); + debug(`Number of hashed edges: ${qEdgeHashes.length}`); const failedHashes = []; - await async.eachSeries(qXEdgeHashes, async (hash) => { + await async.eachSeries(qEdgeHashes, async (hash) => { // lock to prevent caching to/reading from actively caching edge const redisID = 'bte:edgeCache:' + hash; if (parentPort) { @@ -236,7 +236,7 @@ module.exports = class { try { await redisClient.client.delTimeout(redisID); } catch (e) { - debug(`Unable to remove partial cache ${redisID} from redis during cache failure due to error ${error}. This may result in failed or improper cache retrieval of this qXEdge.`) + debug(`Unable to remove partial cache ${redisID} from redis during cache failure due to error ${error}. This may result in failed or improper cache retrieval of this qEdge.`) } } }) @@ -247,7 +247,7 @@ module.exports = class { await redisClient.client.expireTimeout(redisID, process.env.REDIS_KEY_EXPIRE_TIME || 1800); } catch (error) { failedHashes.push(hash); - debug(`Failed to cache qXEdge ${hash} records due to error ${error}. This does not stop other edges from caching nor terminate the query.`) + debug(`Failed to cache qEdge ${hash} records due to error ${error}. This does not stop other edges from caching nor terminate the query.`) } finally { if (parentPort) { parentPort.postMessage({ completeCacheKey: redisID }); @@ -261,7 +261,7 @@ module.exports = class { if (successCount) { debug(`Successfully cached (${successCount}) query records.`); } else { - debug(`qXEdge caching failed.`); + debug(`qEdge caching failed.`); } } catch (error) { debug(`Caching failed due to ${error}. This does not terminate the query.`); diff --git a/src/edge_manager.js b/src/edge_manager.js index 9429d831..c2931965 100644 --- a/src/edge_manager.js +++ b/src/edge_manager.js @@ -5,7 +5,7 @@ const debug = require('debug')('bte:biothings-explorer-trapi:edge-manager'); const config = require('./config'); -module.exports = class QueryExecutionEdgeManager { +module.exports = class QueryEdgeManager { constructor(edges) { // flatten list of all edges available this._qEdges = _.flatten(edges); @@ -42,11 +42,11 @@ module.exports = class QueryExecutionEdgeManager { //either object or subject OR no count last // available not yet executed let available_edges = this._qEdges - .filter(qXEdge => !qXEdge.executed); + .filter(qEdge => !qEdge.executed); //safeguard for making sure there's available //edges when calling getNext if (available_edges.length == 0) { - debug(`(5) Error: ${available_edges} available qXEdges found.`); + debug(`(5) Error: ${available_edges} available qEdges found.`); this.logs.push( new LogEntry( 'DEBUG', @@ -57,48 +57,48 @@ module.exports = class QueryExecutionEdgeManager { ); } //begin search - let nextQXEdge; + let nextQEdge; let lowest_entity_count; let current_obj_lowest = 0; let current_sub_lowest = 0; - available_edges.forEach((qXEdge) => { + available_edges.forEach((qEdge) => { if ( - qXEdge && - qXEdge.object.entity_count + qEdge && + qEdge.object.entity_count ) { - current_obj_lowest = qXEdge.object.entity_count; + current_obj_lowest = qEdge.object.entity_count; if (!lowest_entity_count) { //set current lowest if none lowest_entity_count = current_obj_lowest; } if (current_obj_lowest <= lowest_entity_count) { //lowest is now object count - nextQXEdge = qXEdge; + nextQEdge = qEdge; } } if ( - qXEdge && - qXEdge.subject.entity_count && - qXEdge.subject.entity_count > 0 + qEdge && + qEdge.subject.entity_count && + qEdge.subject.entity_count > 0 ) { - current_sub_lowest = qXEdge.subject.entity_count; + current_sub_lowest = qEdge.subject.entity_count; if (!lowest_entity_count) { //set current lowest if none lowest_entity_count = current_sub_lowest; } if (current_sub_lowest <= lowest_entity_count) { //lowest is now subject count - nextQXEdge = qXEdge; + nextQEdge = qEdge; } } }); - if (!nextQXEdge) { + if (!nextQEdge) { //if no edge with count found pick the first empty //edge available let all_empty = available_edges .filter((edge) => !edge.object.entity_count && !edge.subject.entity_count); if (all_empty.length == 0) { - debug(`(5) Error: No available qXEdges found.`); + debug(`(5) Error: No available qEdges found.`); this.logs.push( new LogEntry( 'DEBUG', @@ -110,27 +110,27 @@ module.exports = class QueryExecutionEdgeManager { debug(`(5) Sending next edge '${all_empty[0].getID()}' with NO entity count.`); return this.preSendOffCheck(all_empty[0]); } - debug(`(5) Sending next edge '${nextQXEdge.getID()}' ` + - `WITH entity count...(${nextQXEdge.subject.entity_count || nextQXEdge.object.entity_count})`); - return this.preSendOffCheck(nextQXEdge); + debug(`(5) Sending next edge '${nextQEdge.getID()}' ` + + `WITH entity count...(${nextQEdge.subject.entity_count || nextQEdge.object.entity_count})`); + return this.preSendOffCheck(nextQEdge); } logEntityCounts() { - this._qEdges.forEach((qXEdge) => { - debug(`'${qXEdge.getID()}'` + - ` : (${qXEdge.subject.entity_count || 0}) ` + - `${qXEdge.reverse ? '<--' : '-->'}` + - ` (${qXEdge.object.entity_count || 0})`); + this._qEdges.forEach((qEdge) => { + debug(`'${qEdge.getID()}'` + + ` : (${qEdge.subject.entity_count || 0}) ` + + `${qEdge.reverse ? '<--' : '-->'}` + + ` (${qEdge.object.entity_count || 0})`); }); } - checkEntityMax(nextQXedge) { + checkEntityMax(nextQEdge) { const max = config.ENTITY_MAX; //(MAX) --- (0) not allowed //(MAX) --- (MAX) not allowed //(MAX) --- (2) allowed, (2 will be used) - let sub_count = nextQXedge.object.getEntityCount(); - let obj_count = nextQXedge.subject.getEntityCount(); + let sub_count = nextQEdge.object.getEntityCount(); + let obj_count = nextQEdge.subject.getEntityCount(); debug(`Checking entity max : (${sub_count})--(${obj_count})`); if ( (obj_count == 0 && sub_count > max) || @@ -138,21 +138,21 @@ module.exports = class QueryExecutionEdgeManager { (obj_count > max && sub_count > max) ) { throw new BTEError( - `Max number of entities exceeded (${max}) in '${nextQXedge.getID()}'` + `Max number of entities exceeded (${max}) in '${nextQEdge.getID()}'` ); } } - preSendOffCheck(nextQXEdge) { - // next: qXEdge + preSendOffCheck(nextQEdge) { + // next: qEdge //check that edge entities are or have potential to stay //under max limit - this.checkEntityMax(nextQXEdge); - if (nextQXEdge.object.entity_count && nextQXEdge.subject.entity_count) { + this.checkEntityMax(nextQEdge); + if (nextQEdge.object.entity_count && nextQEdge.subject.entity_count) { //if at the time of being queried the edge has both //obj and sub entity counts //chose obj/suj lower entity count for query - nextQXEdge.chooseLowerEntityValue(); + nextQEdge.chooseLowerEntityValue(); this.logs.push( new LogEntry('DEBUG', null, @@ -160,21 +160,21 @@ module.exports = class QueryExecutionEdgeManager { ); } else if ( - (nextQXEdge.object.entity_count && !nextQXEdge.subject.entity_count) || - (!nextQXEdge.object.entity_count && !nextQXEdge.subject.entity_count) + (nextQEdge.object.entity_count && !nextQEdge.subject.entity_count) || + (!nextQEdge.object.entity_count && !nextQEdge.subject.entity_count) ) { debug(`(5) Checking direction of edge with one set of entities...`); //check direction is correct if edge only has one set of entities //before sending off - nextQXEdge.reverse = nextQXEdge.subject.entity_count ? false : true; + nextQEdge.reverse = nextQEdge.subject.entity_count ? false : true; } this.logs.push( new LogEntry('DEBUG', null, - `Edge manager is sending next qEdge '${nextQXEdge.getID()}' for execution.`).getLog(), + `Edge manager is sending next qEdge '${nextQEdge.getID()}' for execution.`).getLog(), ); this.logEntityCounts(); - return nextQXEdge; + return nextQEdge; } getEdgesNotExecuted() { @@ -185,17 +185,17 @@ module.exports = class QueryExecutionEdgeManager { return not_executed; } - _filterEdgeRecords(qXEdge) { + _filterEdgeRecords(qEdge) { let keep = []; - let records = qXEdge.records; - let sub_count = qXEdge.subject.curie; - let obj_count = qXEdge.object.curie; - debug(`'${qXEdge.getID()}' Reversed[${qXEdge.reverse}] (${JSON.stringify(sub_count.length || 0)})` + + let records = qEdge.records; + let sub_count = qEdge.subject.curie; + let obj_count = qEdge.object.curie; + debug(`'${qEdge.getID()}' Reversed[${qEdge.reverse}] (${JSON.stringify(sub_count.length || 0)})` + `--(${JSON.stringify(obj_count.length || 0)}) entities / (${records.length}) records.`); // debug(`IDS SUB ${JSON.stringify(sub_count)}`) // debug(`IDS OBJ ${JSON.stringify(obj_count)}`) - let object_node_ids = qXEdge.reverse ? sub_count : obj_count; - let subject_node_ids = qXEdge.reverse ? obj_count : sub_count; + let object_node_ids = qEdge.reverse ? sub_count : obj_count; + let subject_node_ids = qEdge.reverse ? obj_count : sub_count; records.forEach((record) => { //check sub curies against $input ids @@ -288,12 +288,12 @@ module.exports = class QueryExecutionEdgeManager { keep.push(record); } }); - debug(`'${qXEdge.getID()}' dropped (${records.length - keep.length}) records.`); + debug(`'${qEdge.getID()}' dropped (${records.length - keep.length}) records.`); this.logs.push( new LogEntry( 'DEBUG', null, - `'${qXEdge.getID()}' kept (${keep.length}) / dropped (${records.length - keep.length}) records.` + `'${qEdge.getID()}' kept (${keep.length}) / dropped (${records.length - keep.length}) records.` ).getLog(), ); return keep; @@ -308,9 +308,9 @@ module.exports = class QueryExecutionEdgeManager { let brokenEdges = []; debug(`(11) Collecting records...`); //First: go through edges and filter that each edge is holding - this._qEdges.forEach((qXEdge) => { - let qEdgeID = qXEdge.getID(); - let filteredRecords = qXEdge.records.map(record => record.queryDirection()); + this._qEdges.forEach((qEdge) => { + let qEdgeID = qEdge.getID(); + let filteredRecords = qEdge.records.map(record => record.queryDirection()); if (filteredRecords.length == 0) { this.logs.push( new LogEntry( @@ -322,10 +322,10 @@ module.exports = class QueryExecutionEdgeManager { brokenChain = true; brokenEdges.push(qEdgeID); } - this.logs = [...this.logs, ...qXEdge.logs]; + this.logs = [...this.logs, ...qEdge.logs]; //collect records combinedRecords = combinedRecords.concat(filteredRecords); - let connections = qXEdge.subject.getConnections().concat(qXEdge.object.getConnections()); + let connections = qEdge.subject.getConnections().concat(qEdge.object.getConnections()); connections = connections.filter(id => id !== qEdgeID); connections = new Set(connections); recordsByQEdgeID[qEdgeID] = { @@ -352,7 +352,7 @@ module.exports = class QueryExecutionEdgeManager { `resulted in (0) records. No complete paths can be formed.` ).getLog(), ); - debug(`(12) qXEdges ${JSON.stringify(brokenEdges)} ` + + debug(`(12) qEdges ${JSON.stringify(brokenEdges)} ` + `resulted in (0) records. No complete paths can be formed.`); } //Organized by edge: update query records @@ -379,22 +379,22 @@ module.exports = class QueryExecutionEdgeManager { ); } - updateEdgeRecords(currentQXEdge) { + updateEdgeRecords(currentQEdge) { //1. filter edge records based on current status - let filteredRecords = this._filterEdgeRecords(currentQXEdge); + let filteredRecords = this._filterEdgeRecords(currentQEdge); //2.trigger node update / entity update based on new status - currentQXEdge.storeRecords(filteredRecords); + currentQEdge.storeRecords(filteredRecords); } - updateNeighborsEdgeRecords(currentQXEdge) { + updateNeighborsEdgeRecords(currentQEdge) { //update and filter only immediate neighbors debug(`Updating neighbors...`); - let currentQEdgeID = currentQXEdge.getID(); + let currentQEdgeID = currentQEdge.getID(); //get neighbors of this edges subject that are not this edge - let left_connections = currentQXEdge.subject.getConnections(); + let left_connections = currentQEdge.subject.getConnections(); left_connections = left_connections.filter((qEdgeID) => qEdgeID !== currentQEdgeID); //get neighbors of this edges object that are not this edge - let right_connections = currentQXEdge.object.getConnections(); + let right_connections = currentQEdge.object.getConnections(); right_connections = right_connections.filter((qEdgeID) => qEdgeID !== currentQEdgeID); debug(`(${left_connections})<--edge neighbors-->(${right_connections})`); if (left_connections.length) { @@ -422,15 +422,15 @@ module.exports = class QueryExecutionEdgeManager { } } - updateAllOtherEdges(currentQXEdge) { + updateAllOtherEdges(currentQEdge) { //update and filter all other edges debug(`Updating all other edges...`); - let currentQEdgeID = currentQXEdge.getID(); - this._qEdges.forEach((qXEdge) => { - if (qXEdge.getID() !== currentQEdgeID && qXEdge.records.length) { - debug(`Updating "${qXEdge.getID()}"...`); - this.updateEdgeRecords(qXEdge); - this.updateEdgeRecords(currentQXEdge); + let currentQEdgeID = currentQEdge.getID(); + this._qEdges.forEach((qEdge) => { + if (qEdge.getID() !== currentQEdgeID && qEdge.records.length) { + debug(`Updating "${qEdge.getID()}"...`); + this.updateEdgeRecords(qEdge); + this.updateEdgeRecords(currentQEdge); } }); } diff --git a/src/index.js b/src/index.js index d41993c6..f957f99e 100644 --- a/src/index.js +++ b/src/index.js @@ -97,9 +97,9 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { async _processQueryGraph(queryGraph) { try { let queryGraphHandler = new QueryGraph(queryGraph); - let queryExecutionEdges = await queryGraphHandler.calculateEdges(); + let queryEdges = await queryGraphHandler.calculateEdges(); this.logs = [...this.logs, ...queryGraphHandler.logs]; - return queryExecutionEdges; + return queryEdges; } catch (err) { if (err instanceof InvalidQueryGraphError || err instanceof id_resolver.SRIResolverFailiure) { throw err; @@ -110,17 +110,17 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { } } - _createBatchEdgeQueryHandlersForCurrent(currentQXEdge, metaKG) { + _createBatchEdgeQueryHandlersForCurrent(currentQEdge, metaKG) { let handler = new BatchEdgeQueryHandler(metaKG, this.resolveOutputIDs, { caching: this.options.caching, submitter: this.options.submitter, recordHashEdgeAttributes: config.EDGE_ATTRIBUTES_USED_IN_RECORD_HASH, }); - handler.setEdges(currentQXEdge); + handler.setEdges(currentQEdge); return handler; } - async _edgesSupported(qXEdges, metaKG) { + async _edgesSupported(qEdges, metaKG) { if (this.options.dryrun) { let log_msg = 'Running dryrun of query, no API calls will be performed. Actual query execution order may vary based on API responses received.'; @@ -128,26 +128,26 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { } // _.cloneDeep() is resource-intensive but only runs once per query - qXEdges = _.cloneDeep(qXEdges); - const manager = new EdgeManager(qXEdges); + qEdges = _.cloneDeep(qEdges); + const manager = new EdgeManager(qEdges); const qEdgesMissingOps = {}; while (manager.getEdgesNotExecuted()) { - let currentQXEdge = manager.getNext(); - const edgeConverter = new QEdge2APIEdgeHandler([currentQXEdge], metaKG); - const metaXEdges = edgeConverter.getMetaXEdges(currentQXEdge); + let currentQEdge = manager.getNext(); + const edgeConverter = new QEdge2APIEdgeHandler([currentQEdge], metaKG); + const metaXEdges = edgeConverter.getMetaXEdges(currentQEdge); if (this.options.dryrun) { let apiNames = [...new Set(metaXEdges.map((metaXEdge) => metaXEdge.association.api_name))]; let log_msg; - if (currentQXEdge.reverse) { - log_msg = `qEdge ${currentQXEdge.id} (reversed): ${currentQXEdge.object.category} > ${ - currentQXEdge.predicate ? `${currentQXEdge.predicate} > ` : '' - }${currentQXEdge.subject.category}`; + if (currentQEdge.reverse) { + log_msg = `qEdge ${currentQEdge.id} (reversed): ${currentQEdge.object.category} > ${ + currentQEdge.predicate ? `${currentQEdge.predicate} > ` : '' + }${currentQEdge.subject.category}`; } else { - log_msg = `qEdge ${currentQXEdge.id}: ${currentQXEdge.subject.category} > ${ - currentQXEdge.predicate ? `${currentQXEdge.predicate} > ` : '' - }${currentQXEdge.object.category}`; + log_msg = `qEdge ${currentQEdge.id}: ${currentQEdge.subject.category} > ${ + currentQEdge.predicate ? `${currentQEdge.predicate} > ` : '' + }${currentQEdge.object.category}`; } this.logs.push(new LogEntry('INFO', null, log_msg).getLog()); @@ -163,21 +163,21 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { } if (!metaXEdges.length) { - qEdgesMissingOps[currentQXEdge.id] = currentQXEdge.reverse; + qEdgesMissingOps[currentQEdge.id] = currentQEdge.reverse; } // assume results so next edge may be reversed or not - currentQXEdge.executed = true; + currentQEdge.executed = true; //use # of APIs as estimate of # of records if (metaXEdges.length) { - if (currentQXEdge.reverse) { - currentQXEdge.subject.entity_count = currentQXEdge.object.entity_count * metaXEdges.length; + if (currentQEdge.reverse) { + currentQEdge.subject.entity_count = currentQEdge.object.entity_count * metaXEdges.length; } else { - currentQXEdge.object.entity_count = currentQXEdge.subject.entity_count * metaXEdges.length; + currentQEdge.object.entity_count = currentQEdge.subject.entity_count * metaXEdges.length; } } else { - currentQXEdge.object.entity_count = 1; - currentQXEdge.subject.entity_count = 1; + currentQEdge.object.entity_count = 1; + currentQEdge.subject.entity_count = 1; } } @@ -357,7 +357,7 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { ).getLog(), ); } - let queryExecutionEdges = await this._processQueryGraph(this.queryGraph); + let queryEdges = await this._processQueryGraph(this.queryGraph); // TODO remove this when constraints implemented if (await this._checkContraints()) { return; @@ -368,7 +368,7 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { debug(message); return; } - debug(`(3) All edges created ${JSON.stringify(queryExecutionEdges)}`); + debug(`(3) All edges created ${JSON.stringify(queryEdges)}`); if (this._queryUsesInferredMode() && this._queryIsOneHop()) { await this._handleInferredEdges(); return; @@ -378,38 +378,38 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { this.logs.push(new LogEntry('WARNING', null, message).getLog()); return; } - if (!(await this._edgesSupported(queryExecutionEdges, metaKG))) { + if (!(await this._edgesSupported(queryEdges, metaKG))) { return; } - const manager = new EdgeManager(queryExecutionEdges); + const manager = new EdgeManager(queryEdges); const unavailableAPIs = {}; while (manager.getEdgesNotExecuted()) { //next available/most efficient edge - let currentQXEdge = manager.getNext(); + let currentQEdge = manager.getNext(); //crate queries from edge - let handler = this._createBatchEdgeQueryHandlersForCurrent(currentQXEdge, metaKG); + let handler = this._createBatchEdgeQueryHandlersForCurrent(currentQEdge, metaKG); this.logs.push( new LogEntry( 'INFO', null, - `Executing ${currentQXEdge.getID()}${currentQXEdge.isReversed() ? ' (reversed)' : ''}: ${ - currentQXEdge.subject.id - } ${currentQXEdge.isReversed() ? '<--' : '-->'} ${currentQXEdge.object.id}`, + `Executing ${currentQEdge.getID()}${currentQEdge.isReversed() ? ' (reversed)' : ''}: ${ + currentQEdge.subject.id + } ${currentQEdge.isReversed() ? '<--' : '-->'} ${currentQEdge.object.id}`, ).getLog(), ); - debug(`(5) Executing current edge >> "${currentQXEdge.getID()}"`); + debug(`(5) Executing current edge >> "${currentQEdge.getID()}"`); //execute current edge query - let queryRecords = await handler.query(handler.qXEdges, unavailableAPIs); + let queryRecords = await handler.query(handler.qEdges, unavailableAPIs); this.logs = [...this.logs, ...handler.logs]; // create an edge execution summary let success = 0, fail = 0, total = 0; let cached = this.logs.filter( - ({ data }) => data?.qEdgeID === currentQXEdge.id && data?.type === 'cacheHit', + ({ data }) => data?.qEdgeID === currentQEdge.id && data?.type === 'cacheHit', ).length; this.logs - .filter(({ data }) => data?.qEdgeID === currentQXEdge.id && data?.type === 'query') + .filter(({ data }) => data?.qEdgeID === currentQEdge.id && data?.type === 'query') .forEach(({ data }) => { !data.error ? success++ : fail++; total++; @@ -418,43 +418,43 @@ exports.TRAPIQueryHandler = class TRAPIQueryHandler { new LogEntry( 'INFO', null, - `${currentQXEdge.id} execution: ${total} queries (${success} success/${fail} fail) and (${cached}) cached qEdges return (${queryRecords.length}) records`, + `${currentQEdge.id} execution: ${total} queries (${success} success/${fail} fail) and (${cached}) cached qEdges return (${queryRecords.length}) records`, {}, ).getLog(), ); if (queryRecords.length === 0) { this._logSkippedQueries(unavailableAPIs); - debug(`(X) Terminating..."${currentQXEdge.getID()}" got 0 records.`); + debug(`(X) Terminating..."${currentQEdge.getID()}" got 0 records.`); this.logs.push( new LogEntry( 'WARNING', null, - `qEdge (${currentQXEdge.getID()}) got 0 records. Your query terminates.`, + `qEdge (${currentQEdge.getID()}) got 0 records. Your query terminates.`, ).getLog(), ); return; } //storing records will trigger a node entity count update - currentQXEdge.storeRecords(queryRecords); + currentQEdge.storeRecords(queryRecords); //filter records - manager.updateEdgeRecords(currentQXEdge); + manager.updateEdgeRecords(currentQEdge); //update and filter neighbors - manager.updateAllOtherEdges(currentQXEdge); + manager.updateAllOtherEdges(currentQEdge); // check that any records are kept - if (!currentQXEdge.records.length) { + if (!currentQEdge.records.length) { this._logSkippedQueries(unavailableAPIs); - debug(`(X) Terminating..."${currentQXEdge.getID()}" kept 0 records.`); + debug(`(X) Terminating..."${currentQEdge.getID()}" kept 0 records.`); this.logs.push( new LogEntry( 'WARNING', null, - `qEdge (${currentQXEdge.getID()}) kept 0 records. Your query terminates.`, + `qEdge (${currentQEdge.getID()}) kept 0 records. Your query terminates.`, ).getLog(), ); return; } // edge all done - currentQXEdge.executed = true; + currentQEdge.executed = true; debug(`(10) Edge successfully queried.`); } this._logSkippedQueries(unavailableAPIs); diff --git a/src/qedge2apiedge.js b/src/qedge2apiedge.js index 4135b568..9ac3d5ed 100644 --- a/src/qedge2apiedge.js +++ b/src/qedge2apiedge.js @@ -29,34 +29,34 @@ module.exports = class QEdge2APIEdgeHandler { * Get SmartAPI Edges based on TRAPI Query Edge. * @private * @param {object} metaKG - SmartAPI Knowledge Graph Object - * @param {object} qXEdge - TRAPI Query Edge Object + * @param {object} qEdge - TRAPI Query Edge Object */ - getMetaXEdges(qXEdge, metaKG = this.metaKG) { - debug(`Input node is ${qXEdge.getInputNode().id}`); - debug(`Output node is ${qXEdge.getOutputNode().id}`); + getMetaXEdges(qEdge, metaKG = this.metaKG) { + debug(`Input node is ${qEdge.getInputNode().id}`); + debug(`Output node is ${qEdge.getOutputNode().id}`); this.logs.push( new LogEntry( 'DEBUG', null, - `BTE is trying to find metaKG edges (smartAPI registry, x-bte annotation) connecting from ${qXEdge.getInputNode().getCategories()} to ${qXEdge + `BTE is trying to find metaKG edges (smartAPI registry, x-bte annotation) connecting from ${qEdge.getInputNode().getCategories()} to ${qEdge .getOutputNode() - .getCategories()} with predicate ${qXEdge.getPredicate()}`, + .getCategories()} with predicate ${qEdge.getPredicate()}`, ).getLog(), ); let filterCriteria = { - input_type: qXEdge.getInputNode().getCategories(), - output_type: qXEdge.getOutputNode().getCategories(), - predicate: qXEdge.getPredicate(), + input_type: qEdge.getInputNode().getCategories(), + output_type: qEdge.getOutputNode().getCategories(), + predicate: qEdge.getPredicate(), }; debug(`KG Filters: ${JSON.stringify(filterCriteria, null, 2)}`); let metaXEdges = metaKG.filter(filterCriteria).map((metaEdge) => { - metaEdge.reasoner_edge = qXEdge; + metaEdge.reasoner_edge = qEdge; return metaEdge; }); if (metaXEdges.length === 0) { - debug(`No smartapi edge found for ${qXEdge.getID()}`); + debug(`No smartapi edge found for ${qEdge.getID()}`); this.logs.push( - new LogEntry('WARNING', null, `BTE didn't find any metaKG edges corresponding to ${qXEdge.getID()}`).getLog(), + new LogEntry('WARNING', null, `BTE didn't find any metaKG edges corresponding to ${qEdge.getID()}`).getLog(), ); } else { this.logs.push( @@ -65,7 +65,7 @@ module.exports = class QEdge2APIEdgeHandler { null, `BTE found ${ metaXEdges.length - } metaKG edges corresponding to ${qXEdge.getID()}. These metaKG edges comes from ${ + } metaKG edges corresponding to ${qEdge.getID()}. These metaKG edges comes from ${ new Set(this._findAPIsFromMetaEdges(metaXEdges)).size } unique APIs. They are ${Array.from(new Set(this._findAPIsFromMetaEdges(metaXEdges))).join(',')}`, ).getLog(), @@ -320,10 +320,10 @@ module.exports = class QEdge2APIEdgeHandler { return APIEdges; } - async convert(qXEdges) { + async convert(qEdges) { let APIEdges = []; - await Promise.all(qXEdges.map(async (qXEdge) => { - const metaXedges = await this.getMetaXEdges(qXEdge); + await Promise.all(qEdges.map(async (qEdge) => { + const metaXedges = await this.getMetaXEdges(qEdge); const apis = _.uniq(metaXedges.map(api => api.association.api_name)); debug(`${apis.length} APIs being used:`, JSON.stringify(apis)); debug(`${metaXedges.length} SmartAPI edges are retrieved....`); @@ -331,7 +331,7 @@ module.exports = class QEdge2APIEdgeHandler { let newEdges = await this._createAPIEdges(metaXEdge); debug(`${newEdges.length} metaKG are created....`); newEdges = newEdges.map((e) => { - e.filter = qXEdge.filter; + e.filter = qEdge.filter; return e; }); APIEdges = [...APIEdges, ...newEdges]; diff --git a/src/query_edge.js b/src/query_edge.js index 934c4fb0..15ed58b7 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -1,5 +1,5 @@ const helper = require('./helper'); -const debug = require('debug')('bte:biothings-explorer-trapi:MegaQEdge'); +const debug = require('debug')('bte:biothings-explorer-trapi:QEdge'); const utils = require('./utils'); const biolink = require('./biolink'); const { Record } = require('@biothings-explorer/api-response-transform'); @@ -20,25 +20,21 @@ module.exports = class QEdge { this.reverse = this.subject?.getCurie?.() === undefined && this.object?.getCurie?.() !== undefined; - this.reverse = info.reverse !== undefined - ? info.reverse - : this.reverse; + this.reverse = info.reverse !== undefined ? info.reverse : this.reverse; this.reverse = reverse !== undefined ? reverse : this.reverse; this.init(); - //edge has been fully executed this.executed = info.executed === undefined ? false : info.executed; //run initial checks this.logs = info.logs === undefined ? [] : info.logs; //this edges query response records - if (info.records && info.frozen === true) this.records = info.records.map(recordJSON => new Record(recordJSON)); + if (info.records && info.frozen === true) this.records = info.records.map((recordJSON) => new Record(recordJSON)); else this.records = []; - debug(`(2) Created Edge` + - ` ${JSON.stringify(this.getID())} Reverse = ${this.reverse}`) + debug(`(2) Created Edge` + ` ${JSON.stringify(this.getID())} Reverse = ${this.reverse}`); } freeze() { @@ -52,8 +48,8 @@ module.exports = class QEdge { subject: this.subject.freeze(), object: this.object.freeze(), predicate: this.predicate, - records: this.records.map(record => record.freeze()), - frozen: true + records: this.records.map((record) => record.freeze()), + frozen: true, }; } @@ -67,7 +63,10 @@ module.exports = class QEdge { getHashedEdgeRepresentation() { const toBeHashed = - this.getInputNode().getCategories() + this.getPredicate() + this.getOutputNode().getCategories() + this.getInputCurie(); + this.getInputNode().getCategories() + + this.getPredicate() + + this.getOutputNode().getCategories() + + this.getInputCurie(); return helper._generateHash(toBeHashed); } @@ -100,8 +99,7 @@ module.exports = class QEdge { this.reverse = false; this.object.holdCurie(); debug(`(8) Sub - Obj were same but chose subject (${this.subject.entity_count})`); - } - else if (this.object.entity_count > this.subject.entity_count) { + } else if (this.object.entity_count > this.subject.entity_count) { //(#) ---> () this.reverse = false; //tell node to hold curie in a temp field @@ -123,19 +121,18 @@ module.exports = class QEdge { //will give you all curies found by semantic type, each type will have //a main ID and all of it's aliases debug(`(7) Updating Entities in "${this.getID()}"`); - let typesToInclude = isReversed ? - this.subject.getCategories() : - this.object.getCategories(); + let typesToInclude = isReversed ? this.subject.getCategories() : this.object.getCategories(); debug(`(7) Collecting Types: "${JSON.stringify(typesToInclude)}"`); let all = {}; records.forEach((record) => { - record.subject.normalizedInfo.forEach((o) => { //create semantic type if not included let type = o._leafSemanticType; - if (typesToInclude.includes(type) || + if ( + typesToInclude.includes(type) || typesToInclude.includes('NamedThing') || - typesToInclude.toString().includes(type)) { + typesToInclude.toString().includes(type) + ) { if (!Object.hasOwnProperty.call(all, type)) { all[type] = {}; } @@ -182,7 +179,7 @@ module.exports = class QEdge { } //else #2 check curie else if (Object.hasOwnProperty.call(o, 'curie')) { - if (Array.isArray( o.curie)) { + if (Array.isArray(o.curie)) { all[type][original] = o.curie; } else { all[type][original] = [o.curie]; @@ -198,9 +195,11 @@ module.exports = class QEdge { record.object.normalizedInfo.forEach((o) => { //create semantic type if not included let type = o._leafSemanticType; - if (typesToInclude.includes(type) || + if ( + typesToInclude.includes(type) || typesToInclude.includes('NamedThing') || - typesToInclude.toString().includes(type)) { + typesToInclude.toString().includes(type) + ) { if (!Object.hasOwnProperty.call(all, type)) { all[type] = {}; } @@ -248,7 +247,7 @@ module.exports = class QEdge { } //else #2 check curie else if (Object.hasOwnProperty.call(o, 'curie')) { - if (Array.isArray( o.curie)) { + if (Array.isArray(o.curie)) { all[type][original] = o.curie; } else { all[type][original] = [o.curie]; @@ -260,7 +259,6 @@ module.exports = class QEdge { } } }); - }); // {Gene:{'id': ['alias']}} debug(`Collected entity ids in records: ${JSON.stringify(Object.keys(all))}`); @@ -271,7 +269,7 @@ module.exports = class QEdge { //combine all curies in case there are //multiple categories in this node since //they are separated by type - let combined = {}; + let combined = {}; for (const type in curies) { for (const original in curies[type]) { combined[original] = curies[type][original]; @@ -284,15 +282,11 @@ module.exports = class QEdge { //update node queried (1) ---> (update) let curies_by_semantic_type = this.extractCuriesFromRecords(records, this.reverse); let combined_curies = this._combineCuries(curies_by_semantic_type); - this.reverse ? - this.subject.updateCuries(combined_curies) : - this.object.updateCuries(combined_curies); + this.reverse ? this.subject.updateCuries(combined_curies) : this.object.updateCuries(combined_curies); //update node used as input (1 [update]) ---> () let curies_by_semantic_type_2 = this.extractCuriesFromRecords(records, !this.reverse); let combined_curies_2 = this._combineCuries(curies_by_semantic_type_2); - !this.reverse ? - this.subject.updateCuries(combined_curies_2) : - this.object.updateCuries(combined_curies_2); + !this.reverse ? this.subject.updateCuries(combined_curies_2) : this.object.updateCuries(combined_curies_2); } applyNodeConstraints() { @@ -310,7 +304,7 @@ module.exports = class QEdge { //apply constraints for (let x = 0; x < sub_constraints.length; x++) { const constraint = sub_constraints[x]; - keep = this.meetsConstraint(constraint, res, from) + keep = this.meetsConstraint(constraint, res, from); } //pass or not if (keep) { @@ -330,7 +324,7 @@ module.exports = class QEdge { //apply constraints for (let x = 0; x < obj_constraints.length; x++) { const constraint = obj_constraints[x]; - keep = this.meetsConstraint(constraint, res, from) + keep = this.meetsConstraint(constraint, res, from); } //pass or not if (keep) { @@ -340,7 +334,7 @@ module.exports = class QEdge { } if (save_kept) { //only override recordss if there was any filtering done. - this.records = kept; + this.records = kept; debug(`(6) Reduced to (${this.records.length}) records.`); } else { debug(`(6) No constraints. Skipping...`); @@ -351,7 +345,7 @@ module.exports = class QEdge { //list of attribute ids in node let available_attributes = new Set(); for (const key in record[from].attributes) { - available_attributes.add(key) + available_attributes.add(key); } available_attributes = [...available_attributes]; // debug(`ATTRS ${JSON.stringify(record[from].normalizedInfo[0]._leafSemanticType)}` + @@ -368,53 +362,59 @@ module.exports = class QEdge { node_attributes[filter] = record[from].attributes[filter]; }); switch (constraint.operator) { - case "==": - for (const key in node_attributes) { - if (!isNaN(constraint.value)) { - if (Array.isArray(node_attributes[key])) { - if (node_attributes[key].includes(constraint.value) || - node_attributes[key].includes(constraint.value.toString())) { - return true; - } - } else { - if (node_attributes[key] == constraint.value || - node_attributes[key] == constraint.value.toString() || - node_attributes[key] == parseInt(constraint.value)) { - return true; - } + case '==': + for (const key in node_attributes) { + if (!isNaN(constraint.value)) { + if (Array.isArray(node_attributes[key])) { + if ( + node_attributes[key].includes(constraint.value) || + node_attributes[key].includes(constraint.value.toString()) + ) { + return true; } } else { - if (Array.isArray(node_attributes[key])) { - if (node_attributes[key].includes(constraint.value)) { - return true; - } - } else { - if (node_attributes[key] == constraint.value || - node_attributes[key] == constraint.value.toString() || - node_attributes[key] == parseInt(constraint.value)) { - return true; - } + if ( + node_attributes[key] == constraint.value || + node_attributes[key] == constraint.value.toString() || + node_attributes[key] == parseInt(constraint.value) + ) { + return true; } } - } - return false; - case ">": - for (const key in node_attributes) { + } else { if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) > parseInt(constraint.value)) { - return true; - } + if (node_attributes[key].includes(constraint.value)) { + return true; } } else { - if (parseInt(node_attributes[key]) > parseInt(constraint.value)) { + if ( + node_attributes[key] == constraint.value || + node_attributes[key] == constraint.value.toString() || + node_attributes[key] == parseInt(constraint.value) + ) { return true; } } } - return false; - case ">=": + } + return false; + case '>': + for (const key in node_attributes) { + if (Array.isArray(node_attributes[key])) { + for (let index = 0; index < node_attributes[key].length; index++) { + const element = node_attributes[key][index]; + if (parseInt(element) > parseInt(constraint.value)) { + return true; + } + } + } else { + if (parseInt(node_attributes[key]) > parseInt(constraint.value)) { + return true; + } + } + } + return false; + case '>=': for (const key in node_attributes) { if (Array.isArray(node_attributes[key])) { for (let index = 0; index < node_attributes[key].length; index++) { @@ -430,7 +430,7 @@ module.exports = class QEdge { } } return false; - case "<": + case '<': for (const key in node_attributes) { if (Array.isArray(node_attributes[key])) { for (let index = 0; index < node_attributes[key].length; index++) { @@ -446,7 +446,7 @@ module.exports = class QEdge { } } return false; - case "<=": + case '<=': for (const key in node_attributes) { if (Array.isArray(node_attributes[key])) { for (let index = 0; index < node_attributes[key].length; index++) { @@ -465,7 +465,7 @@ module.exports = class QEdge { default: debug(`Node operator not handled ${constraint.operator}`); return false; - }; + } } } @@ -481,7 +481,10 @@ module.exports = class QEdge { getHashedEdgeRepresentation() { const toBeHashed = - this.getInputNode().getCategories() + this.getPredicate() + this.getOutputNode().getCategories() + this.getInputCurie(); + this.getInputNode().getCategories() + + this.getPredicate() + + this.getOutputNode().getCategories() + + this.getInputCurie(); return helper._generateHash(toBeHashed); } diff --git a/src/query_node.js b/src/query_node.js index f897b343..33c07f62 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -1,240 +1,239 @@ const _ = require('lodash'); const utils = require('./utils'); const biolink = require('./biolink'); -const debug = require('debug')('bte:biothings-explorer-trapi:NewQNode'); +const debug = require('debug')('bte:biothings-explorer-trapi:QNode'); const InvalidQueryGraphError = require('./exceptions/invalid_query_graph_error'); module.exports = class QNode { - /** - * - * @param {object} info - Qnode info, e.g. ID, curie, category - */ - constructor(info) { - this.id = info.id; - this.category = info.categories || 'NamedThing'; - // mainIDs - this.curie = info.ids; - //is_set - this.is_set = info.is_set; - //mainID : its equivalent ids - this.expanded_curie = info.expanded_curie !== undefined ? info.expanded_curie : {}; - this.entity_count = info.ids ? info.ids.length : 0; - debug(`(1) Node "${this.id}" has (${this.entity_count}) entities at start.`); - //when choosing a lower entity count a node with higher count - // might be told to store its curies temporarily - this.held_curie = info.held_curie !== undefined ? info.held_curie : []; - this.held_expanded = info.held_expanded !== undefined ? info.held_expanded : {}; - //node constraints - this.constraints = info.constraints; - //list of edge ids that are connected to this node - this.connected_to = info.connected_to !== undefined ? new Set(info.connected_to) : new Set(); - //object-ify array of initial curies - if (info.expanded_curie === undefined) this.expandCurie(); - this.validateConstraints(); - } - - freeze() { - return { - category: this.category, - connected_to: Array.from(this.connected_to), - constraints: this.constraints, - curie: this.curie, - entity_count: this.entity_count, - equivalentIDs: this.equivalentIDs, - expanded_curie: this.expanded_curie, - held_curie: this.held_curie, - held_expanded: this.held_expanded, - id: this.id, - is_set: this.is_set - } - } - - isSet() { - //query node specified as set - return this.is_set ? true : false; - } - - validateConstraints() { - const required = ['id', 'operator', 'value']; - if (this.constraints && this.constraints.length) { - this.constraints.forEach((constraint) => { - let constraint_keys = Object.keys(constraint); - if (_.intersection(constraint_keys, required).length < 3) { - throw new InvalidQueryGraphError( - `Invalid constraint specification must include (${required})`); - } - }); - } - } - - expandCurie() { - if (this.curie && this.curie.length) { - this.curie.forEach((id) => { - if (!Object.hasOwnProperty.call(id, this.expanded_curie)) { - this.expanded_curie[id] = [id]; - } - }); - debug(`(1) Node "${this.id}" expanded initial curie. ${JSON.stringify(this.expanded_curie)}`); - } - } - - updateConnection(qEdgeID) { - this.connected_to.add(qEdgeID); - debug(`"${this.id}" connected to "${[...this.connected_to]}"`); - } - - getConnections() { - return [...this.connected_to]; - } - - holdCurie() { - //hold curie aside temp - debug(`(8) Node "${this.id}" holding ${JSON.stringify(this.curie)} aside.`); - this.held_curie = this.curie; - this.held_expanded = this.expanded_curie; - this.curie = undefined; - this.expanded_curie = {}; - } - - updateCuries(curies) { - // {originalID : [aliases]} - if (!this.curie) { - this.curie = []; - } - //bring back held curie - if (this.held_curie.length) { - debug(`(8) Node "${this.id}" restored curie.`); - //restore - this.curie = this.held_curie; - this.expanded_curie = this.held_expanded; - //reset holds - this.held_curie = []; - this.held_expanded = {}; - } - if (!this.curie.length) { - debug(`Node "${this.id}" saving (${Object.keys(curies).length}) curies...`); - this.curie = Object.keys(curies); - this.expanded_curie = curies; - } else { - debug(`Node "${this.id}" intersecting (${this.curie.length})/(${Object.keys(curies).length}) curies...`); - // let intersection = this.intersectCuries(this.curie, curies); - // this.curie = intersection; - // debug(`Node "${this.id}" kept (${intersection.length}) curies...`); - this.intersectWithExpandedCuries(curies); - } - this.entity_count = this.curie.length; - } - - _combineCuriesIntoList(curies) { - // curies {originalID : ['aliasID']} - //combine all curies into single list for easy intersection - let combined = new Set(); - for (const original in curies) { - !Array.isArray(curies[original]) ? - combined.add(curies[original]) : - curies[original].forEach((curie) => { - combined.add(curie); - }); - } - return [...combined]; - } - - intersectWithExpandedCuries(newCuries) { - let keep = {}; - for (const mainID in newCuries) { - let current_list_of_aliases = newCuries[mainID]; - for (const existingMainID in this.expanded_curie) { - let existing_list_of_aliases = this.expanded_curie[existingMainID]; - let idsMatchFound = _.intersection(current_list_of_aliases, existing_list_of_aliases); - if (idsMatchFound.length) { - if (!Object.hasOwnProperty.call(keep, mainID)) { - keep[mainID] = current_list_of_aliases; - } - } - } + /** + * + * @param {object} info - Qnode info, e.g. ID, curie, category + */ + constructor(info) { + this.id = info.id; + this.category = info.categories || 'NamedThing'; + // mainIDs + this.curie = info.ids; + //is_set + this.is_set = info.is_set; + //mainID : its equivalent ids + this.expanded_curie = info.expanded_curie !== undefined ? info.expanded_curie : {}; + this.entity_count = info.ids ? info.ids.length : 0; + debug(`(1) Node "${this.id}" has (${this.entity_count}) entities at start.`); + //when choosing a lower entity count a node with higher count + // might be told to store its curies temporarily + this.held_curie = info.held_curie !== undefined ? info.held_curie : []; + this.held_expanded = info.held_expanded !== undefined ? info.held_expanded : {}; + //node constraints + this.constraints = info.constraints; + //list of edge ids that are connected to this node + this.connected_to = info.connected_to !== undefined ? new Set(info.connected_to) : new Set(); + //object-ify array of initial curies + if (info.expanded_curie === undefined) this.expandCurie(); + this.validateConstraints(); + } + + freeze() { + return { + category: this.category, + connected_to: Array.from(this.connected_to), + constraints: this.constraints, + curie: this.curie, + entity_count: this.entity_count, + equivalentIDs: this.equivalentIDs, + expanded_curie: this.expanded_curie, + held_curie: this.held_curie, + held_expanded: this.held_expanded, + id: this.id, + is_set: this.is_set, + }; + } + + isSet() { + //query node specified as set + return this.is_set ? true : false; + } + + validateConstraints() { + const required = ['id', 'operator', 'value']; + if (this.constraints && this.constraints.length) { + this.constraints.forEach((constraint) => { + let constraint_keys = Object.keys(constraint); + if (_.intersection(constraint_keys, required).length < 3) { + throw new InvalidQueryGraphError(`Invalid constraint specification must include (${required})`); } - //save expanded curies (main + aliases) - this.expanded_curie = keep; - //save curies (main ids) - this.curie = Object.keys(keep); - debug(`Node "${this.id}" kept (${Object.keys(keep).length}) curies...`); - } - - intersectCuries(curies, newCuries) { - //curies is a list ['ID'] - // new curies {originalID : ['aliasID']} - let all_new_curies = this._combineCuriesIntoList(newCuries); - return _.intersection(curies, all_new_curies ); - } - - getID() { - return this.id; - } - - getCurie() { - return this.curie; + }); } + } - getEquivalentIDs() { - return this.equivalentIDs ?? {}; - } - - removeEquivalentID(id) { - delete this.equivalentIDs[id]; - } - - getCategories() { - if (this.hasEquivalentIDs() === false) { - const categories = utils.toArray(this.category); - let expanded_categories = []; - categories.map((category) => { - expanded_categories = [ - ...expanded_categories, - ...biolink.getDescendantClasses(utils.removeBioLinkPrefix(category)), - ]; - }); - return utils.getUnique(expanded_categories); + expandCurie() { + if (this.curie && this.curie.length) { + this.curie.forEach((id) => { + if (!Object.hasOwnProperty.call(id, this.expanded_curie)) { + this.expanded_curie[id] = [id]; } - let categories = []; - Object.values(this.equivalentIDs).map((entities) => { - entities.map((entity) => { - categories = [...categories, ...entity.semanticTypes.map(semantic => utils.removeBioLinkPrefix(semantic))]; - }); - }); - return utils.getUnique(categories); - } - - getEntities() { - return Object.values(this.equivalentIDs).reduce((res, entities) => { - return [...res, ...entities]; - }, []); - } - - getPrimaryIDs() { - return this.getEntities().map((entity) => entity.primaryID); - } - - setEquivalentIDs(equivalentIDs) { - this.equivalentIDs = equivalentIDs; - } - - updateEquivalentIDs(equivalentIDs) { - if (this.equivalentIDs === undefined) { - this.equivalentIDs = equivalentIDs; - } else { - this.equivalentIDs = { ...this.equivalentIDs, ...equivalentIDs }; + }); + debug(`(1) Node "${this.id}" expanded initial curie. ${JSON.stringify(this.expanded_curie)}`); + } + } + + updateConnection(qEdgeID) { + this.connected_to.add(qEdgeID); + debug(`"${this.id}" connected to "${[...this.connected_to]}"`); + } + + getConnections() { + return [...this.connected_to]; + } + + holdCurie() { + //hold curie aside temp + debug(`(8) Node "${this.id}" holding ${JSON.stringify(this.curie)} aside.`); + this.held_curie = this.curie; + this.held_expanded = this.expanded_curie; + this.curie = undefined; + this.expanded_curie = {}; + } + + updateCuries(curies) { + // {originalID : [aliases]} + if (!this.curie) { + this.curie = []; + } + //bring back held curie + if (this.held_curie.length) { + debug(`(8) Node "${this.id}" restored curie.`); + //restore + this.curie = this.held_curie; + this.expanded_curie = this.held_expanded; + //reset holds + this.held_curie = []; + this.held_expanded = {}; + } + if (!this.curie.length) { + debug(`Node "${this.id}" saving (${Object.keys(curies).length}) curies...`); + this.curie = Object.keys(curies); + this.expanded_curie = curies; + } else { + debug(`Node "${this.id}" intersecting (${this.curie.length})/(${Object.keys(curies).length}) curies...`); + // let intersection = this.intersectCuries(this.curie, curies); + // this.curie = intersection; + // debug(`Node "${this.id}" kept (${intersection.length}) curies...`); + this.intersectWithExpandedCuries(curies); + } + this.entity_count = this.curie.length; + } + + _combineCuriesIntoList(curies) { + // curies {originalID : ['aliasID']} + //combine all curies into single list for easy intersection + let combined = new Set(); + for (const original in curies) { + !Array.isArray(curies[original]) + ? combined.add(curies[original]) + : curies[original].forEach((curie) => { + combined.add(curie); + }); + } + return [...combined]; + } + + intersectWithExpandedCuries(newCuries) { + let keep = {}; + for (const mainID in newCuries) { + let current_list_of_aliases = newCuries[mainID]; + for (const existingMainID in this.expanded_curie) { + let existing_list_of_aliases = this.expanded_curie[existingMainID]; + let idsMatchFound = _.intersection(current_list_of_aliases, existing_list_of_aliases); + if (idsMatchFound.length) { + if (!Object.hasOwnProperty.call(keep, mainID)) { + keep[mainID] = current_list_of_aliases; + } } + } } - - hasInput() { - return !(this.curie === undefined || this.curie === null); - } - - hasEquivalentIDs() { - return !(typeof this.equivalentIDs === 'undefined' || this.equivalentIDs === {}); - } - - getEntityCount() { - return this.curie ? this.curie.length : 0; - } + //save expanded curies (main + aliases) + this.expanded_curie = keep; + //save curies (main ids) + this.curie = Object.keys(keep); + debug(`Node "${this.id}" kept (${Object.keys(keep).length}) curies...`); + } + + intersectCuries(curies, newCuries) { + //curies is a list ['ID'] + // new curies {originalID : ['aliasID']} + let all_new_curies = this._combineCuriesIntoList(newCuries); + return _.intersection(curies, all_new_curies); + } + + getID() { + return this.id; + } + + getCurie() { + return this.curie; + } + + getEquivalentIDs() { + return this.equivalentIDs ?? {}; + } + + removeEquivalentID(id) { + delete this.equivalentIDs[id]; + } + + getCategories() { + if (this.hasEquivalentIDs() === false) { + const categories = utils.toArray(this.category); + let expanded_categories = []; + categories.map((category) => { + expanded_categories = [ + ...expanded_categories, + ...biolink.getDescendantClasses(utils.removeBioLinkPrefix(category)), + ]; + }); + return utils.getUnique(expanded_categories); + } + let categories = []; + Object.values(this.equivalentIDs).map((entities) => { + entities.map((entity) => { + categories = [...categories, ...entity.semanticTypes.map((semantic) => utils.removeBioLinkPrefix(semantic))]; + }); + }); + return utils.getUnique(categories); + } + + getEntities() { + return Object.values(this.equivalentIDs).reduce((res, entities) => { + return [...res, ...entities]; + }, []); + } + + getPrimaryIDs() { + return this.getEntities().map((entity) => entity.primaryID); + } + + setEquivalentIDs(equivalentIDs) { + this.equivalentIDs = equivalentIDs; + } + + updateEquivalentIDs(equivalentIDs) { + if (this.equivalentIDs === undefined) { + this.equivalentIDs = equivalentIDs; + } else { + this.equivalentIDs = { ...this.equivalentIDs, ...equivalentIDs }; + } + } + + hasInput() { + return !(this.curie === undefined || this.curie === null); + } + + hasEquivalentIDs() { + return !(typeof this.equivalentIDs === 'undefined' || this.equivalentIDs === {}); + } + + getEntityCount() { + return this.curie ? this.curie.length : 0; + } }; diff --git a/src/update_nodes.js b/src/update_nodes.js index c530c459..019a1968 100644 --- a/src/update_nodes.js +++ b/src/update_nodes.js @@ -2,27 +2,24 @@ const id_resolver = require('biomedical_id_resolver'); const debug = require('debug')('bte:biothings-explorer-trapi:nodeUpdateHandler'); module.exports = class NodesUpdateHandler { - constructor(qXEdges) { - this.qXEdges = qXEdges; + constructor(qEdges) { + this.qEdges = qEdges; } /** * @private * s */ - _getCuries(qXEdges) { + _getCuries(qEdges) { let curies = {}; - qXEdges.map((qXEdge) => { - if (qXEdge.hasInputResolved()) { - return; - } - if (qXEdge.hasInput()) { - const inputCategories = qXEdge.getInputNode().getCategories(); + qEdges.map((qEdge) => { + if (qEdge.hasInput()) { + const inputCategories = qEdge.getInputNode().getCategories(); inputCategories.map((category) => { if (!(category in curies)) { curies[category] = []; } - curies[category] = [...curies[category], ...qXEdge.getInputCurie()]; + curies[category] = [...curies[category], ...qEdge.getInputCurie()]; }); } }); @@ -40,20 +37,20 @@ module.exports = class NodesUpdateHandler { return equivalentIDs; } - async setEquivalentIDs(qXEdges) { + async setEquivalentIDs(qEdges) { debug(`Getting equivalent IDs...`); - const curies = this._getCuries(this.qXEdges); + const curies = this._getCuries(this.qEdges); debug(`curies: ${JSON.stringify(curies)}`); const equivalentIDs = await this._getEquivalentIDs(curies); - qXEdges.map((qXEdge) => { + qEdges.map((qEdge) => { const edgeEquivalentIDs = Object.keys(equivalentIDs) - .filter((key) => qXEdge.getInputCurie().includes(key)) + .filter((key) => qEdge.getInputCurie().includes(key)) .reduce((res, key) => { return { ...res, [key]: equivalentIDs[key] }; }, {}); - debug(`Got Edge Equivalent IDs successfully.`); + debug(`Got Edge Equivalent IDs successfully.`); if (Object.keys(edgeEquivalentIDs).length > 0) { - qXEdge.getInputNode().setEquivalentIDs(edgeEquivalentIDs); + qEdge.getInputNode().setEquivalentIDs(edgeEquivalentIDs); } }); return; @@ -80,11 +77,8 @@ module.exports = class NodesUpdateHandler { // ); // }) queryRecords.map((record) => { - if ( - record && - !(record.object.curie in record.qXEdge.getOutputNode().getEquivalentIDs()) - ) { - record.qXEdge.getOutputNode().updateEquivalentIDs({[record.object.curie]: record.object.normalizedInfo}); + if (record && !(record.object.curie in record.qEdge.getOutputNode().getEquivalentIDs())) { + record.qEdge.getOutputNode().updateEquivalentIDs({ [record.object.curie]: record.object.normalizedInfo }); } }); } From 11b58585ac308053aa26a19508553445f9f1f2a4 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Mon, 31 Oct 2022 13:48:46 -0400 Subject: [PATCH 15/34] test: fix test references --- .../integration/QEdge2BTEEdgeHandler.test.js | 13 +++-- __test__/unittest/cacheHandler.test.js | 58 +++++++++---------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/__test__/integration/QEdge2BTEEdgeHandler.test.js b/__test__/integration/QEdge2BTEEdgeHandler.test.js index 7cbd01ce..c751a89c 100644 --- a/__test__/integration/QEdge2BTEEdgeHandler.test.js +++ b/__test__/integration/QEdge2BTEEdgeHandler.test.js @@ -5,12 +5,15 @@ const NodeUpdateHandler = require('../../src/update_nodes'); describe('Testing NodeUpdateHandler Module', () => { const gene_node1 = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:1017'] }); const node1_equivalent_ids = { - 'NCBIGene:1017': { - db_ids: { - NCBIGene: ['1017'], - SYMBOL: ['CDK2'], + 'NCBIGene:1017': [ + { + semanticTypes: [], + db_ids: { + NCBIGene: ['1017'], + SYMBOL: ['CDK2'], + }, }, - }, + ], }; const gene_node2 = new QNode({ id: 'n2', categories: ['Gene'], ids: ['NCBIGene:1017', 'NCBIGene:1018'] }); diff --git a/__test__/unittest/cacheHandler.test.js b/__test__/unittest/cacheHandler.test.js index 9c497801..c644b80e 100644 --- a/__test__/unittest/cacheHandler.test.js +++ b/__test__/unittest/cacheHandler.test.js @@ -4,7 +4,7 @@ const { Readable } = require('stream'); const { Record } = require('@biothings-explorer/api-response-transform'); const Redis = require('ioredis-mock'); -const qXedges = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../data/qXEdges.json')), { encoding: 'utf8' }); +const qEdges = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../data/qEdges.json')), { encoding: 'utf8' }); const records = Record.unfreezeRecords( JSON.parse(fs.readFileSync(path.resolve(__dirname, '../data/queryRecords.json')), { encoding: 'utf8' }), @@ -31,19 +31,19 @@ describe('test cache handler', () => { const cacheHandler = new CacheHandler(false); const categorizeEdges = jest.spyOn(CacheHandler.prototype, 'categorizeEdges'); const _hashEdgeByMetaKG = jest.spyOn(CacheHandler.prototype, '_hashEdgeByMetaKG'); - const _groupQueryRecordsByQXEdgeHash = jest.spyOn(CacheHandler.prototype, '_groupQueryRecordsByQXEdgeHash'); + const _groupQueryRecordsByQEdgeHash = jest.spyOn(CacheHandler.prototype, '_groupQueryRecordsByQEdgeHash'); expect(cacheHandler.cacheEnabled).toBeFalsy(); - const { cachedRecords, nonCachedQXEdges } = await cacheHandler.categorizeEdges(qXedges); + const { cachedRecords, nonCachedQEdges } = await cacheHandler.categorizeEdges(qEdges); expect(categorizeEdges).toHaveBeenCalledTimes(1); expect(_hashEdgeByMetaKG).toHaveBeenCalledTimes(0); expect(cachedRecords).toHaveLength(0); - expect(nonCachedQXEdges).toHaveLength(1); - expect(nonCachedQXEdges).toEqual(qXedges); + expect(nonCachedQEdges).toHaveLength(1); + expect(nonCachedQEdges).toEqual(qEdges); await cacheHandler.cacheEdges(records); - expect(_groupQueryRecordsByQXEdgeHash).toHaveBeenCalledTimes(0); + expect(_groupQueryRecordsByQEdgeHash).toHaveBeenCalledTimes(0); }); test("don't use cache when explicitely disabled by ENV", async () => { @@ -54,19 +54,19 @@ describe('test cache handler', () => { const cacheHandler = new CacheHandler(true); const categorizeEdges = jest.spyOn(CacheHandler.prototype, 'categorizeEdges'); const _hashEdgeByMetaKG = jest.spyOn(CacheHandler.prototype, '_hashEdgeByMetaKG'); - const _groupQueryRecordsByQXEdgeHash = jest.spyOn(CacheHandler.prototype, '_groupQueryRecordsByQXEdgeHash'); + const _groupQueryRecordsByQEdgeHash = jest.spyOn(CacheHandler.prototype, '_groupQueryRecordsByQEdgeHash'); expect(cacheHandler.cacheEnabled).toBeFalsy(); - const { cachedRecords, nonCachedQXEdges } = await cacheHandler.categorizeEdges(qXedges); + const { cachedRecords, nonCachedQEdges } = await cacheHandler.categorizeEdges(qEdges); expect(categorizeEdges).toHaveBeenCalledTimes(1); expect(_hashEdgeByMetaKG).toHaveBeenCalledTimes(0); expect(cachedRecords).toHaveLength(0); - expect(nonCachedQXEdges).toHaveLength(1); - expect(nonCachedQXEdges).toEqual(qXedges); + expect(nonCachedQEdges).toHaveLength(1); + expect(nonCachedQEdges).toEqual(qEdges); await cacheHandler.cacheEdges(records); - expect(_groupQueryRecordsByQXEdgeHash).toHaveBeenCalledTimes(0); + expect(_groupQueryRecordsByQEdgeHash).toHaveBeenCalledTimes(0); }); test("don't use cache when redis disabled", async () => { @@ -74,19 +74,19 @@ describe('test cache handler', () => { const cacheHandler = new CacheHandler(true); const categorizeEdges = jest.spyOn(CacheHandler.prototype, 'categorizeEdges'); const _hashEdgeByMetaKG = jest.spyOn(CacheHandler.prototype, '_hashEdgeByMetaKG'); - const _groupQueryRecordsByQXEdgeHash = jest.spyOn(CacheHandler.prototype, '_groupQueryRecordsByQXEdgeHash'); + const _groupQueryRecordsByQEdgeHash = jest.spyOn(CacheHandler.prototype, '_groupQueryRecordsByQEdgeHash'); expect(cacheHandler.cacheEnabled).toBeFalsy(); - const { cachedRecords, nonCachedQXEdges } = await cacheHandler.categorizeEdges(qXedges); + const { cachedRecords, nonCachedQEdges } = await cacheHandler.categorizeEdges(qEdges); expect(categorizeEdges).toHaveBeenCalledTimes(1); expect(_hashEdgeByMetaKG).toHaveBeenCalledTimes(0); expect(cachedRecords).toHaveLength(0); - expect(nonCachedQXEdges).toHaveLength(1); - expect(nonCachedQXEdges).toEqual(qXedges); + expect(nonCachedQEdges).toHaveLength(1); + expect(nonCachedQEdges).toEqual(qEdges); await cacheHandler.cacheEdges(records); - expect(_groupQueryRecordsByQXEdgeHash).toHaveBeenCalledTimes(0); + expect(_groupQueryRecordsByQEdgeHash).toHaveBeenCalledTimes(0); }); test("don't use cache when redis specially disabled", async () => { @@ -97,17 +97,17 @@ describe('test cache handler', () => { const cacheHandler = new CacheHandler(true); const categorizeEdges = jest.spyOn(CacheHandler.prototype, 'categorizeEdges'); const _hashEdgeByMetaKG = jest.spyOn(CacheHandler.prototype, '_hashEdgeByMetaKG'); - const _groupQueryRecordsByQXEdgeHash = jest.spyOn(CacheHandler.prototype, '_groupQueryRecordsByQXEdgeHash'); + const _groupQueryRecordsByQEdgeHash = jest.spyOn(CacheHandler.prototype, '_groupQueryRecordsByQEdgeHash'); - const { cachedRecords, nonCachedQXEdges } = await cacheHandler.categorizeEdges(qXedges); + const { cachedRecords, nonCachedQEdges } = await cacheHandler.categorizeEdges(qEdges); expect(categorizeEdges).toHaveBeenCalledTimes(1); expect(_hashEdgeByMetaKG).toHaveBeenCalledTimes(0); expect(cachedRecords).toHaveLength(0); - expect(nonCachedQXEdges).toHaveLength(1); - expect(nonCachedQXEdges).toEqual(qXedges); + expect(nonCachedQEdges).toHaveLength(1); + expect(nonCachedQEdges).toEqual(qEdges); await cacheHandler.cacheEdges(records); - expect(_groupQueryRecordsByQXEdgeHash).toHaveBeenCalledTimes(0); + expect(_groupQueryRecordsByQEdgeHash).toHaveBeenCalledTimes(0); }); }); @@ -240,15 +240,15 @@ describe('test cache handler', () => { }); }); - test('_groupQueryRecordsByQXEdgeHash', () => { + test('_groupQueryRecordsByQEdgeHash', () => { process.env.REDIS_HOST = 'mocked'; process.env.REDIS_PORT = 'mocked'; const CacheHandler = require('../../src/cache_handler'); const cacheHandler = new CacheHandler(true); - const groups = cacheHandler._groupQueryRecordsByQXEdgeHash(records); + const groups = cacheHandler._groupQueryRecordsByQEdgeHash(records); const numHashes = records.reduce((set, record) => { - set.add(record.qXEdge.getHashedEdgeRepresentation()); + set.add(record.qEdge.getHashedEdgeRepresentation()); return set; }, new Set()).size; @@ -269,16 +269,16 @@ describe('test cache handler', () => { const redisClient = new Redis(); await cacheHandler.cacheEdges(records); - const qXEdges = Object.values( + const qEdges = Object.values( records.reduce((obj, record) => { - if (!(record.qXEdge.getHashedEdgeRepresentation() in obj)) { - obj[record.qXEdge.getHashedEdgeRepresentation()] = record.qXEdge; + if (!(record.qEdge.getHashedEdgeRepresentation() in obj)) { + obj[record.qEdge.getHashedEdgeRepresentation()] = record.qEdge; } return obj; }, {}), ); - const { cachedRecords, nonCachedQXEdges } = await cacheHandler.categorizeEdges(qXEdges); - expect(nonCachedQXEdges).toHaveLength(0); + const { cachedRecords, nonCachedQEdges } = await cacheHandler.categorizeEdges(qEdges); + expect(nonCachedQEdges).toHaveLength(0); expect(cachedRecords).toHaveLength(records.length); // TODO get each record sorted by hash to compare individually const originalRecordHashes = records.reduce((set, record) => { From cf80a4e49b5d6a44192ab836a3414e0e9110d839 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:01:24 -0400 Subject: [PATCH 16/34] test: rename test data file --- __test__/data/{qXEdges.json => qEdges.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename __test__/data/{qXEdges.json => qEdges.json} (100%) diff --git a/__test__/data/qXEdges.json b/__test__/data/qEdges.json similarity index 100% rename from __test__/data/qXEdges.json rename to __test__/data/qEdges.json From 033d30c82235fde574ea12bb322783da2f81cad0 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Mon, 31 Oct 2022 16:00:14 -0400 Subject: [PATCH 17/34] fix: qualifiers optional for ease of test writing --- src/graph/knowledge_graph.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph/knowledge_graph.js b/src/graph/knowledge_graph.js index 96d8d38f..b149ce4d 100644 --- a/src/graph/knowledge_graph.js +++ b/src/graph/knowledge_graph.js @@ -65,7 +65,7 @@ module.exports = class KnowledgeGraph { } _createQualifiers(kgEdge) { - const qualifiers = Object.entries(kgEdge.qualifiers).map(([qualifierType, qualifier]) => { + const qualifiers = Object.entries(kgEdge.qualifiers || {}).map(([qualifierType, qualifier]) => { return { qualifier_type_id: qualifierType, qualifier_value: qualifier, From 97adb197c338921f2446b0b8a5b013a02ea1aa94 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Thu, 3 Nov 2022 13:30:43 -0400 Subject: [PATCH 18/34] feat: support qualifier-constraints in QEdge --- src/query_edge.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/query_edge.js b/src/query_edge.js index 15ed58b7..1c229552 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -17,6 +17,7 @@ module.exports = class QEdge { this.subject = info.frozen === true ? new QNode(info.subject) : info.subject; this.object = info.frozen === true ? new QNode(info.object) : info.object; this.expanded_predicates = []; + this.qualifier_constraints = info.qualifier_constraints; this.reverse = this.subject?.getCurie?.() === undefined && this.object?.getCurie?.() !== undefined; @@ -42,6 +43,7 @@ module.exports = class QEdge { id: this.id, predicate: this.predicate, expanded_predicates: this.expanded_predicates, + qualifier_constraints: this.qualifier_constraints, executed: this.executed, reverse: this.reverse, logs: this.logs, @@ -89,6 +91,41 @@ module.exports = class QEdge { .filter((item) => !(typeof item === 'undefined')); } + getQualifierConstraints() { + if (this.isReversed()) { + return this.qualifier_constraints.map((qualifierSetObj) => { + return { + qualifier_set: qualifierSetObj.qualifier_set.map(({qualifier_type_id, qualifier_value}) => { + let newQualifierType; + let newQualifierValue; + if (qualifier_type_id.includes("predicate")) { + newQualifierValue = this.getReversedPredicate(qualifier_value); + } + if (qualifier_type_id.includes("subject")) { + newQualifierType = qualifier_type_id.replace("subject", "object"); + } + if (qualifier_type_id.includes("object")) { + newQualifierType = qualifier_type_id.replace("object", "subject"); + } + return { + qualifier_type_id: newQualifierType, + qualifier_value: newQualifierValue, + } + }) + } + }); + } + return this.qualifier_constraints; + } + + getSimpleQualifierConstraints() { + return this.getQualifierConstraints().map((qualifierSetObj) => { + return Object.fromEntries( + qualifierSetObj.qualifier_set.map(({qualifier_type_id, qualifier_value}) => [qualifier_type_id, qualifier_value]), + ); + }); + } + chooseLowerEntityValue() { //edge has both subject and object entity counts and must choose lower value //to use in query. From 89e5f2ffe3ea24276e7e7daaee319928e20c680a Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Thu, 3 Nov 2022 13:34:05 -0400 Subject: [PATCH 19/34] fix: undefined qualifier support --- src/query_edge.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/query_edge.js b/src/query_edge.js index 1c229552..b6098dda 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -92,27 +92,30 @@ module.exports = class QEdge { } getQualifierConstraints() { + if (!this.qualifier_constraints) { + return; + } if (this.isReversed()) { return this.qualifier_constraints.map((qualifierSetObj) => { return { - qualifier_set: qualifierSetObj.qualifier_set.map(({qualifier_type_id, qualifier_value}) => { + qualifier_set: qualifierSetObj.qualifier_set.map(({ qualifier_type_id, qualifier_value }) => { let newQualifierType; let newQualifierValue; - if (qualifier_type_id.includes("predicate")) { + if (qualifier_type_id.includes('predicate')) { newQualifierValue = this.getReversedPredicate(qualifier_value); } - if (qualifier_type_id.includes("subject")) { - newQualifierType = qualifier_type_id.replace("subject", "object"); + if (qualifier_type_id.includes('subject')) { + newQualifierType = qualifier_type_id.replace('subject', 'object'); } - if (qualifier_type_id.includes("object")) { - newQualifierType = qualifier_type_id.replace("object", "subject"); + if (qualifier_type_id.includes('object')) { + newQualifierType = qualifier_type_id.replace('object', 'subject'); } return { qualifier_type_id: newQualifierType, qualifier_value: newQualifierValue, - } - }) - } + }; + }), + }; }); } return this.qualifier_constraints; @@ -121,7 +124,10 @@ module.exports = class QEdge { getSimpleQualifierConstraints() { return this.getQualifierConstraints().map((qualifierSetObj) => { return Object.fromEntries( - qualifierSetObj.qualifier_set.map(({qualifier_type_id, qualifier_value}) => [qualifier_type_id, qualifier_value]), + qualifierSetObj.qualifier_set.map(({ qualifier_type_id, qualifier_value }) => [ + qualifier_type_id, + qualifier_value, + ]), ); }); } From 3a0ca1aa3543f99077579994d328939437318f8a Mon Sep 17 00:00:00 2001 From: Colleen Xu Date: Fri, 4 Nov 2022 22:34:23 -0700 Subject: [PATCH 20/34] adjust templates for predicate changes with qualifier-refactor --- data/templates/Drug-treats-Disease/m4-Chem-Gene-DoP.json | 2 +- .../less-promising/m2-Disease-subclassDoP-Gene-Chem.json | 2 +- .../templates/less-promising/m3-Disease-SeqVar-Gene-Chem.json | 2 +- data/templates/less-promising/m5-Disease-Pheno-Gene-Chem.json | 4 ++-- data/templates/less-promising/m6-Disease-Gene-Gene-Chem.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/templates/Drug-treats-Disease/m4-Chem-Gene-DoP.json b/data/templates/Drug-treats-Disease/m4-Chem-Gene-DoP.json index 077f5f0c..2a9d9a10 100644 --- a/data/templates/Drug-treats-Disease/m4-Chem-Gene-DoP.json +++ b/data/templates/Drug-treats-Disease/m4-Chem-Gene-DoP.json @@ -17,7 +17,7 @@ "eA": { "subject": "creativeQuerySubject", "object": "nA", - "predicates": ["biolink:entity_regulates_entity"] + "predicates": ["biolink:regulates", "biolink:affects"] }, "eB": { "subject": "nA", diff --git a/data/templates/less-promising/m2-Disease-subclassDoP-Gene-Chem.json b/data/templates/less-promising/m2-Disease-subclassDoP-Gene-Chem.json index 484c5408..5769bbef 100644 --- a/data/templates/less-promising/m2-Disease-subclassDoP-Gene-Chem.json +++ b/data/templates/less-promising/m2-Disease-subclassDoP-Gene-Chem.json @@ -31,7 +31,7 @@ "eC": { "subject": "nB", "object": "creativeQuerySubject", - "predicates": ["biolink:entity_regulated_by_entity"] + "predicates": ["biolink:regulated_by", "biolink:affected_by"] } } } diff --git a/data/templates/less-promising/m3-Disease-SeqVar-Gene-Chem.json b/data/templates/less-promising/m3-Disease-SeqVar-Gene-Chem.json index eb72a082..a10398ed 100644 --- a/data/templates/less-promising/m3-Disease-SeqVar-Gene-Chem.json +++ b/data/templates/less-promising/m3-Disease-SeqVar-Gene-Chem.json @@ -29,7 +29,7 @@ "eC": { "subject": "nB", "object": "creativeQuerySubject", - "predicates": ["biolink:entity_regulated_by_entity"] + "predicates": ["biolink:regulated_by", "biolink:affected_by"] } } } diff --git a/data/templates/less-promising/m5-Disease-Pheno-Gene-Chem.json b/data/templates/less-promising/m5-Disease-Pheno-Gene-Chem.json index c8540e4e..a9faa79d 100644 --- a/data/templates/less-promising/m5-Disease-Pheno-Gene-Chem.json +++ b/data/templates/less-promising/m5-Disease-Pheno-Gene-Chem.json @@ -26,12 +26,12 @@ "eB": { "subject": "nA", "object": "nB", - "predicates": ["biolink:entity_regulated_by_entity"] + "predicates": ["biolink:regulated_by", "biolink:affected_by"] }, "eC": { "subject": "nB", "object": "creativeQuerySubject", - "predicates": ["biolink:entity_regulated_by_entity"] + "predicates": ["biolink:regulated_by", "biolink:affected_by"] } } } diff --git a/data/templates/less-promising/m6-Disease-Gene-Gene-Chem.json b/data/templates/less-promising/m6-Disease-Gene-Gene-Chem.json index a0f4860f..fccf5ca4 100644 --- a/data/templates/less-promising/m6-Disease-Gene-Gene-Chem.json +++ b/data/templates/less-promising/m6-Disease-Gene-Gene-Chem.json @@ -31,7 +31,7 @@ "eC": { "subject": "nB", "object": "creativeQuerySubject", - "predicates": ["biolink:entity_regulated_by_entity"] + "predicates": ["biolink:regulated_by", "biolink:affected_by"] } } } From 7ce695662a735c35e0391319045daa5523b1d734 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Mon, 7 Nov 2022 13:25:57 -0500 Subject: [PATCH 21/34] fix: strip biolink prefix on qualifier constraints --- src/query_edge.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/query_edge.js b/src/query_edge.js index b6098dda..8782729c 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -17,7 +17,7 @@ module.exports = class QEdge { this.subject = info.frozen === true ? new QNode(info.subject) : info.subject; this.object = info.frozen === true ? new QNode(info.object) : info.object; this.expanded_predicates = []; - this.qualifier_constraints = info.qualifier_constraints; + this.qualifier_constraints = info.qualifier_constraints || []; this.reverse = this.subject?.getCurie?.() === undefined && this.object?.getCurie?.() !== undefined; @@ -93,7 +93,7 @@ module.exports = class QEdge { getQualifierConstraints() { if (!this.qualifier_constraints) { - return; + return []; } if (this.isReversed()) { return this.qualifier_constraints.map((qualifierSetObj) => { @@ -125,7 +125,7 @@ module.exports = class QEdge { return this.getQualifierConstraints().map((qualifierSetObj) => { return Object.fromEntries( qualifierSetObj.qualifier_set.map(({ qualifier_type_id, qualifier_value }) => [ - qualifier_type_id, + qualifier_type_id.replace('biolink:', ''), qualifier_value, ]), ); From db5b9bc182ddb3ae8dfba99908e97ae1c4862c9e Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Tue, 15 Nov 2022 13:16:32 -0500 Subject: [PATCH 22/34] fix: pass qualifier constraints --- src/qedge2apiedge.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qedge2apiedge.js b/src/qedge2apiedge.js index 9ac3d5ed..e165f8c1 100644 --- a/src/qedge2apiedge.js +++ b/src/qedge2apiedge.js @@ -47,6 +47,7 @@ module.exports = class QEdge2APIEdgeHandler { input_type: qEdge.getInputNode().getCategories(), output_type: qEdge.getOutputNode().getCategories(), predicate: qEdge.getPredicate(), + qualifiers: qEdge.getSimpleQualifierConstraints(), }; debug(`KG Filters: ${JSON.stringify(filterCriteria, null, 2)}`); let metaXEdges = metaKG.filter(filterCriteria).map((metaEdge) => { From d995447aa2c146fac041f8cae4d2f9e54a1adce2 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Tue, 15 Nov 2022 13:16:46 -0500 Subject: [PATCH 23/34] fix: qualifier reversing --- src/query_edge.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/query_edge.js b/src/query_edge.js index 8782729c..79a69169 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -99,10 +99,10 @@ module.exports = class QEdge { return this.qualifier_constraints.map((qualifierSetObj) => { return { qualifier_set: qualifierSetObj.qualifier_set.map(({ qualifier_type_id, qualifier_value }) => { - let newQualifierType; - let newQualifierValue; + let newQualifierType = qualifier_type_id; + let newQualifierValue = qualifier_value; if (qualifier_type_id.includes('predicate')) { - newQualifierValue = this.getReversedPredicate(qualifier_value); + newQualifierValue = `biolink:${this.getReversedPredicate(qualifier_value.replace('biolink:', ''))}`; } if (qualifier_type_id.includes('subject')) { newQualifierType = qualifier_type_id.replace('subject', 'object'); From fc0c6428834a90b35fbd829e0754c4b33a8ecadb Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Tue, 15 Nov 2022 13:17:01 -0500 Subject: [PATCH 24/34] feat: qualifier duplicate type validation --- src/query_graph.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/query_graph.js b/src/query_graph.js index 4cc96bd9..8c7b2360 100644 --- a/src/query_graph.js +++ b/src/query_graph.js @@ -93,6 +93,24 @@ module.exports = class QueryGraphHandler { } } + _validateNoDuplicateQualifierTypes(queryGraph) { + Object.entries(queryGraph.edges).forEach(([id, edge]) => { + if (edge.qualifier_constraints) { + edge.qualifier_constraints.forEach((qualifierSet, i) => { + const qualifierTypes = new Set(); + qualifierSet.qualifier_set.forEach(({ qualifier_type_id }) => { + if (qualifierTypes.has(qualifier_type_id)) { + throw new InvalidQueryGraphError( + `Query edge ${id} qualifier set ${i} contains duplicate qualifier_type_id ${qualifier_type_id}`, + ); + } + qualifierTypes.add(qualifier_type_id); + }); + }); + } + }); + } + _validate(queryGraph) { this._validateEmptyEdges(queryGraph); this._validateEmptyNodes(queryGraph); @@ -100,6 +118,7 @@ module.exports = class QueryGraphHandler { this._validateNodeEdgeCorrespondence(queryGraph); this._validateDuplicateEdges(queryGraph); this._validateCycles(queryGraph); + this._validateNoDuplicateQualifierTypes(queryGraph); } /** From a820bdaaac2a9694661b0e27e3edcb2ba1e11ece Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Thu, 1 Dec 2022 13:52:13 -0500 Subject: [PATCH 25/34] fix: merged result count for logging --- src/inferred_mode/inferred_mode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inferred_mode/inferred_mode.js b/src/inferred_mode/inferred_mode.js index c61e29a2..51d6271f 100644 --- a/src/inferred_mode/inferred_mode.js +++ b/src/inferred_mode/inferred_mode.js @@ -372,7 +372,7 @@ module.exports = class InferredQueryHandler { if (queryHadResults) resultQueries.push(i); Object.entries(mergedResults).forEach((result, countMerged) => { mergedResultsCount[result] = - result in mergedResultsCount ? countMerged : mergedResultsCount[result] + countMerged; + result in mergedResultsCount ? mergedResultsCount[result] + countMerged : countMerged; }); // log to user if we should stop if (creativeLimitHit) { From fbc95d501f79100b1f83b9dadddd25fd94528f7c Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Thu, 1 Dec 2022 13:52:49 -0500 Subject: [PATCH 26/34] fix: don't use too-general categories --- src/biolink.js | 16 ++++++++++++++++ src/query_node.js | 9 ++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/biolink.js b/src/biolink.js index c1471177..1843f1d1 100644 --- a/src/biolink.js +++ b/src/biolink.js @@ -26,6 +26,22 @@ class BioLinkModel { return undefined; } + getAncestorClasses(className) { + if (className in this.biolink.classTree.objects) { + const ancestors = this.biolink.classTree.getAncestors(className).map((entity) => entity.name); + return [...ancestors, ...[className]]; + } + return className; + } + + getAncestorPredicates(predicate) { + if (predicate in this.biolink.slotTree.objects) { + const ancestors = this.biolink.slotTree.getAncestors(predicate).map((entity) => entity.name); + return [...ancestors, ...[predicate]]; + } + return predicate; + } + getDescendantClasses(className) { if (className in this.biolink.classTree.objects) { const descendants = this.biolink.classTree.getDescendants(className).map((entity) => entity.name); diff --git a/src/query_node.js b/src/query_node.js index 33c07f62..c0c7ec1d 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -194,13 +194,20 @@ module.exports = class QNode { }); return utils.getUnique(expanded_categories); } + let ancestors = new Set( + utils + .toArray(this.category) + .map((category) => utils.removeBioLinkPrefix(category)) + .reduce((arr, category) => [...arr, ...biolink.getAncestorClasses(category)], []) + .filter((category) => !utils.toArray(this.category).includes(`biolink:${category}`)), + ); let categories = []; Object.values(this.equivalentIDs).map((entities) => { entities.map((entity) => { categories = [...categories, ...entity.semanticTypes.map((semantic) => utils.removeBioLinkPrefix(semantic))]; }); }); - return utils.getUnique(categories); + return utils.getUnique(categories).filter(category => !ancestors.has(category)); } getEntities() { From 606e8b18d30b4e1038b96415f1559aa28bbf11ea Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Fri, 2 Dec 2022 13:31:52 -0500 Subject: [PATCH 27/34] fix: simpler method using equivalentIDs semanticType --- src/query_node.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/query_node.js b/src/query_node.js index c0c7ec1d..c9fc3dff 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -194,20 +194,23 @@ module.exports = class QNode { }); return utils.getUnique(expanded_categories); } - let ancestors = new Set( - utils - .toArray(this.category) - .map((category) => utils.removeBioLinkPrefix(category)) - .reduce((arr, category) => [...arr, ...biolink.getAncestorClasses(category)], []) - .filter((category) => !utils.toArray(this.category).includes(`biolink:${category}`)), - ); - let categories = []; + // let ancestors = new Set( + // utils + // .toArray(this.category) + // .map((category) => utils.removeBioLinkPrefix(category)) + // .reduce((arr, category) => [...arr, ...biolink.getAncestorClasses(category)], []) + // .filter((category) => !utils.toArray(this.category).includes(`biolink:${category}`)), + // ); + let categories = utils.toArray(this.category).map((category) => utils.removeBioLinkPrefix(category)); Object.values(this.equivalentIDs).map((entities) => { entities.map((entity) => { - categories = [...categories, ...entity.semanticTypes.map((semantic) => utils.removeBioLinkPrefix(semantic))]; + categories = [...categories, entity.semanticType]; }); }); - return utils.getUnique(categories).filter(category => !ancestors.has(category)); + return utils.getUnique( + utils.getUnique(categories).reduce((arr, category) => [...arr, ...biolink.getDescendantClasses(category)], []), + ); + // .filter(category => !ancestors.has(category)); } getEntities() { From 3c80f50a0ed2d4577ca4b09a1fa94ee3afe550d6 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Wed, 7 Dec 2022 14:53:51 -0500 Subject: [PATCH 28/34] fix: filtering w/ no qualifiers, qualified predicates, reversing --- src/query_edge.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/query_edge.js b/src/query_edge.js index 79a69169..da89df70 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -122,14 +122,15 @@ module.exports = class QEdge { } getSimpleQualifierConstraints() { - return this.getQualifierConstraints().map((qualifierSetObj) => { + const constraints = this.getQualifierConstraints().map((qualifierSetObj) => { return Object.fromEntries( qualifierSetObj.qualifier_set.map(({ qualifier_type_id, qualifier_value }) => [ qualifier_type_id.replace('biolink:', ''), - qualifier_value, + qualifier_value.replace('biolink:', ''), ]), ); }); + return constraints.length > 0 ? constraints : undefined; } chooseLowerEntityValue() { From 3fc1d9366c4be26c97ee991d66fe50e581e004b6 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Wed, 7 Dec 2022 15:56:08 -0500 Subject: [PATCH 29/34] fix: use qualifiers in hash, hash order-agnostic --- src/query_edge.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/query_edge.js b/src/query_edge.js index da89df70..99d2d708 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -64,11 +64,22 @@ module.exports = class QEdge { } getHashedEdgeRepresentation() { + // all values sorted so same qEdge with slightly different orders will hash the same + const qualifiersSorted = this.getSimpleQualifierConstraints() + .map((qualifierSet) => { + return Object.entries(qualifierSet) + .sort(([qTa, qVa], [qTb, qVb]) => qTa.localeCompare(qTb)) + .reduce((str, [qType, qVal]) => `${str}${qType}:${qVal};`, ''); + }) + .sort((setString1, setString2) => setString1.localeCompare(setString2)); + const toBeHashed = - this.getInputNode().getCategories() + - this.getPredicate() + - this.getOutputNode().getCategories() + - this.getInputCurie(); + this.getInputNode().getCategories().sort() + + this.getPredicate().sort() + + this.getOutputNode().getCategories().sort() + + this.getInputCurie().sort() + + qualifiersSorted; + return helper._generateHash(toBeHashed); } @@ -523,15 +534,6 @@ module.exports = class QEdge { this.updateNodesCuries(records); } - getHashedEdgeRepresentation() { - const toBeHashed = - this.getInputNode().getCategories() + - this.getPredicate() + - this.getOutputNode().getCategories() + - this.getInputCurie(); - return helper._generateHash(toBeHashed); - } - expandPredicates(predicates) { const reducer = (acc, cur) => [...acc, ...biolink.getDescendantPredicates(cur)]; return Array.from(new Set(predicates.reduce(reducer, []))); From 825d8d63fc42e1f0736e225c303765d7a9656b55 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:01:30 -0500 Subject: [PATCH 30/34] fix: hash sort when undefined, rm duplicate methods --- src/query_edge.js | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/query_edge.js b/src/query_edge.js index 99d2d708..7169f327 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -74,10 +74,10 @@ module.exports = class QEdge { .sort((setString1, setString2) => setString1.localeCompare(setString2)); const toBeHashed = - this.getInputNode().getCategories().sort() + - this.getPredicate().sort() + - this.getOutputNode().getCategories().sort() + - this.getInputCurie().sort() + + (this.getInputNode().getCategories() || []).sort() + + (this.getPredicate() || []).sort() + + (this.getOutputNode().getCategories() || []).sort() + + (this.getInputCurie() || []).sort() + qualifiersSorted; return helper._generateHash(toBeHashed); @@ -539,20 +539,6 @@ module.exports = class QEdge { return Array.from(new Set(predicates.reduce(reducer, []))); } - getPredicate() { - if (this.predicate === undefined || this.predicate === null) { - return undefined; - } - const predicates = utils.toArray(this.predicate).map((item) => utils.removeBioLinkPrefix(item)); - const expandedPredicates = this.expandPredicates(predicates); - debug(`Expanded edges: ${expandedPredicates}`); - return expandedPredicates - .map((predicate) => { - return this.isReversed() === true ? biolink.reverse(predicate) : predicate; - }) - .filter((item) => !(typeof item === 'undefined')); - } - getInputNode() { if (this.reverse) { return this.object; @@ -579,14 +565,6 @@ module.exports = class QEdge { return [curie]; } - getInputNode() { - return this.reverse ? this.object : this.subject; - } - - getOutputNode() { - return this.reverse ? this.subject : this.object; - } - hasInputResolved() { return this.getInputNode().hasEquivalentIDs(); } From 1f94388e49cc7b535fbdfce53bf0fe389d9e1e03 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:59:25 -0500 Subject: [PATCH 31/34] fix: iteration of potentially undefined response --- src/query_edge.js | 2 +- src/query_node.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/query_edge.js b/src/query_edge.js index 7169f327..6cbf136c 100644 --- a/src/query_edge.js +++ b/src/query_edge.js @@ -65,7 +65,7 @@ module.exports = class QEdge { getHashedEdgeRepresentation() { // all values sorted so same qEdge with slightly different orders will hash the same - const qualifiersSorted = this.getSimpleQualifierConstraints() + const qualifiersSorted = (this.getSimpleQualifierConstraints() || []) .map((qualifierSet) => { return Object.entries(qualifierSet) .sort(([qTa, qVa], [qTb, qVb]) => qTa.localeCompare(qTb)) diff --git a/src/query_node.js b/src/query_node.js index c9fc3dff..a3f04e95 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -189,7 +189,7 @@ module.exports = class QNode { categories.map((category) => { expanded_categories = [ ...expanded_categories, - ...biolink.getDescendantClasses(utils.removeBioLinkPrefix(category)), + ...(biolink.getDescendantClasses(utils.removeBioLinkPrefix(category)) || []), ]; }); return utils.getUnique(expanded_categories); @@ -208,7 +208,7 @@ module.exports = class QNode { }); }); return utils.getUnique( - utils.getUnique(categories).reduce((arr, category) => [...arr, ...biolink.getDescendantClasses(category)], []), + utils.getUnique(categories).reduce((arr, category) => [...arr, ...(biolink.getDescendantClasses(category) || [])], []), ); // .filter(category => !ancestors.has(category)); } From 32b6bc10026ffa0f7850fc0ead99ccffcf400603 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:59:40 -0500 Subject: [PATCH 32/34] test: fix tests --- .../integration/QEdge2BTEEdgeHandler.test.js | 11 ++-- __test__/integration/QueryNode.test.js | 7 ++- __test__/integration/graph/graph.test.js | 60 +++++++++---------- __test__/integration/integrity.test.js | 12 ++-- 4 files changed, 48 insertions(+), 42 deletions(-) diff --git a/__test__/integration/QEdge2BTEEdgeHandler.test.js b/__test__/integration/QEdge2BTEEdgeHandler.test.js index c751a89c..2a8fa058 100644 --- a/__test__/integration/QEdge2BTEEdgeHandler.test.js +++ b/__test__/integration/QEdge2BTEEdgeHandler.test.js @@ -39,11 +39,12 @@ describe('Testing NodeUpdateHandler Module', () => { expect(res.Gene.length).toEqual(2); }); - test('test edge with input node annotated should return an empty array', () => { - const nodeUpdater = new NodeUpdateHandler([edge2]); - const res = nodeUpdater._getCuries([edge2]); - expect(res).toEqual({}); - }); + // test deprecated: proper update handling outside of updater ensures minimal redundancy + // test('test edge with input node annotated should return an empty array', () => { + // const nodeUpdater = new NodeUpdateHandler([edge2]); + // const res = nodeUpdater._getCuries([edge2]); + // expect(res).toEqual({}); + // }); test('test edge with input on object end should be handled', () => { const nodeUpdater = new NodeUpdateHandler([edge4]); diff --git a/__test__/integration/QueryNode.test.js b/__test__/integration/QueryNode.test.js index 7ec4934b..ff852de9 100644 --- a/__test__/integration/QueryNode.test.js +++ b/__test__/integration/QueryNode.test.js @@ -142,24 +142,27 @@ describe('Testing QueryNode Module', () => { expect(node.getCategories()).toEqual(['Gene']); }); - test('If equivalent ids are not empty, return all semantic types defined in the entity', () => { + test('If equivalent ids are not empty, return all primary semantic types defined in equivalent entities', () => { const node = new QNode({ id: 'n1', categories: 'Gene' }); node.equivalentIDs = { A: [ { + semanticType: 'm', semanticTypes: ['m', 'n'], }, { + semanticType: 'p', semanticTypes: ['p', 'q'], }, ], B: [ { + semanticType: 'x', semanticTypes: ['x', 'y'], }, ], }; - expect(node.getCategories()).toEqual(['m', 'n', 'p', 'q', 'x', 'y']); + expect(node.getCategories()).toEqual(['Gene', 'm', 'p', 'x']); }); }); }); diff --git a/__test__/integration/graph/graph.test.js b/__test__/integration/graph/graph.test.js index 5841dc5c..318d5575 100644 --- a/__test__/integration/graph/graph.test.js +++ b/__test__/integration/graph/graph.test.js @@ -76,11 +76,11 @@ describe("Test graph class", () => { expect(g.nodes["inputPrimaryCurie-qg1"]._qNodeID).toEqual("qg1"); expect(Array.from(g.nodes["inputPrimaryCurie-qg1"]._targetNodes)).toEqual(['outputPrimaryCurie-qg2']); expect(Array.from(g.nodes["inputPrimaryCurie-qg1"]._targetQNodeIDs)).toEqual(['qg2']); - expect(g.edges).toHaveProperty('ead33fce1d19dc004679aa389eca7ff4'); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].apis)).toEqual(['API1']); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].sources)).toEqual(['source1']); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].publications)).toEqual(['PMID:1', 'PMID:2']); - expect(g.edges['ead33fce1d19dc004679aa389eca7ff4'].attributes).toHaveProperty('relation', 'relation1') + expect(g.edges).toHaveProperty('95fe2a8089c0d79ea093b97c5991f7ba'); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].apis)).toEqual(['API1']); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].sources)).toEqual(['source1']); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].publications)).toEqual(['PMID:1', 'PMID:2']); + expect(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].attributes).toHaveProperty('relation', 'relation1') }) test("Multiple query results are correctly updated for two edges having same input, predicate and output", () => { @@ -97,17 +97,17 @@ describe("Test graph class", () => { expect(Array.from(g.nodes["inputPrimaryCurie-qg1"]._targetNodes)).toEqual(['outputPrimaryCurie-qg2']); expect(Array.from(g.nodes["inputPrimaryCurie-qg1"]._targetQNodeIDs)).toEqual(['qg2']); - expect(g.edges).toHaveProperty('ead33fce1d19dc004679aa389eca7ff4'); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].apis)).toEqual(['API1']); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].sources)).toEqual(['source1']); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].publications)).toEqual(['PMID:1', 'PMID:2']); - expect(g.edges['ead33fce1d19dc004679aa389eca7ff4'].attributes).toHaveProperty('relation', 'relation1') + expect(g.edges).toHaveProperty('95fe2a8089c0d79ea093b97c5991f7ba'); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].apis)).toEqual(['API1']); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].sources)).toEqual(['source1']); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].publications)).toEqual(['PMID:1', 'PMID:2']); + expect(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].attributes).toHaveProperty('relation', 'relation1') - expect(g.edges).toHaveProperty('37a029a060de5df47516d73e7d2a0d19'); - expect(Array.from(g.edges['37a029a060de5df47516d73e7d2a0d19'].apis)).toEqual(['API2']); - expect(Array.from(g.edges['37a029a060de5df47516d73e7d2a0d19'].sources)).toEqual(['source2']); - expect(Array.from(g.edges['37a029a060de5df47516d73e7d2a0d19'].publications)).toEqual(['PMC:1', 'PMC:2']); - expect(g.edges['37a029a060de5df47516d73e7d2a0d19'].attributes).toHaveProperty('relation', 'relation2') + expect(g.edges).toHaveProperty('9d334cb674d5671364c45cc8403184c6'); + expect(Array.from(g.edges['9d334cb674d5671364c45cc8403184c6'].apis)).toEqual(['API2']); + expect(Array.from(g.edges['9d334cb674d5671364c45cc8403184c6'].sources)).toEqual(['source2']); + expect(Array.from(g.edges['9d334cb674d5671364c45cc8403184c6'].publications)).toEqual(['PMC:1', 'PMC:2']); + expect(g.edges['9d334cb674d5671364c45cc8403184c6'].attributes).toHaveProperty('relation', 'relation2') }) test("Multiple query results for different edges are correctly updated", () => { @@ -124,22 +124,22 @@ describe("Test graph class", () => { expect(Array.from(g.nodes["inputPrimaryCurie-qg1"]._targetNodes)).toEqual(['outputPrimaryCurie-qg2']); expect(Array.from(g.nodes["inputPrimaryCurie-qg1"]._targetQNodeIDs)).toEqual(['qg2']); - expect(g.edges).toHaveProperty('ead33fce1d19dc004679aa389eca7ff4'); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].apis)).toEqual(['API1']); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].sources)).toEqual(['source1']); - expect(Array.from(g.edges['ead33fce1d19dc004679aa389eca7ff4'].publications)).toEqual(['PMID:1', 'PMID:2']); - expect(g.edges['ead33fce1d19dc004679aa389eca7ff4'].attributes).toHaveProperty('relation', 'relation1') + expect(g.edges).toHaveProperty('95fe2a8089c0d79ea093b97c5991f7ba'); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].apis)).toEqual(['API1']); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].sources)).toEqual(['source1']); + expect(Array.from(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].publications)).toEqual(['PMID:1', 'PMID:2']); + expect(g.edges['95fe2a8089c0d79ea093b97c5991f7ba'].attributes).toHaveProperty('relation', 'relation1') - expect(g.edges).toHaveProperty('37a029a060de5df47516d73e7d2a0d19'); - expect(Array.from(g.edges['37a029a060de5df47516d73e7d2a0d19'].apis)).toEqual(['API2']); - expect(Array.from(g.edges['37a029a060de5df47516d73e7d2a0d19'].sources)).toEqual(['source2']); - expect(Array.from(g.edges['37a029a060de5df47516d73e7d2a0d19'].publications)).toEqual(['PMC:1', 'PMC:2']); - expect(g.edges['37a029a060de5df47516d73e7d2a0d19'].attributes).toHaveProperty('relation', 'relation2') + expect(g.edges).toHaveProperty('9d334cb674d5671364c45cc8403184c6'); + expect(Array.from(g.edges['9d334cb674d5671364c45cc8403184c6'].apis)).toEqual(['API2']); + expect(Array.from(g.edges['9d334cb674d5671364c45cc8403184c6'].sources)).toEqual(['source2']); + expect(Array.from(g.edges['9d334cb674d5671364c45cc8403184c6'].publications)).toEqual(['PMC:1', 'PMC:2']); + expect(g.edges['9d334cb674d5671364c45cc8403184c6'].attributes).toHaveProperty('relation', 'relation2') - expect(g.edges).toHaveProperty('30b7230795a102faeac8fe417b477524'); - expect(Array.from(g.edges['30b7230795a102faeac8fe417b477524'].apis)).toEqual(['API3']); - expect(Array.from(g.edges['30b7230795a102faeac8fe417b477524'].sources)).toEqual(['source3']); - expect(Array.from(g.edges['30b7230795a102faeac8fe417b477524'].publications)).toEqual(['PMC:3', 'PMC:4']); - expect(g.edges['30b7230795a102faeac8fe417b477524'].attributes).toHaveProperty('relation', 'relation3') + expect(g.edges).toHaveProperty('4fe2d5d3e03e0f78f272745caf6b627d'); + expect(Array.from(g.edges['4fe2d5d3e03e0f78f272745caf6b627d'].apis)).toEqual(['API3']); + expect(Array.from(g.edges['4fe2d5d3e03e0f78f272745caf6b627d'].sources)).toEqual(['source3']); + expect(Array.from(g.edges['4fe2d5d3e03e0f78f272745caf6b627d'].publications)).toEqual(['PMC:3', 'PMC:4']); + expect(g.edges['4fe2d5d3e03e0f78f272745caf6b627d'].attributes).toHaveProperty('relation', 'relation3') }) }) diff --git a/__test__/integration/integrity.test.js b/__test__/integration/integrity.test.js index d451749e..abbbaf04 100644 --- a/__test__/integration/integrity.test.js +++ b/__test__/integration/integrity.test.js @@ -3,14 +3,14 @@ const fs = require('fs'); var path = require('path'); describe('Testing TRAPIQueryHandler Module', () => { - const example_foler = path.resolve(__dirname, '../data'); + const example_folder = path.resolve(__dirname, '../data'); //skip until we figure out why it returns no results //https://suwulab.slack.com/archives/CC218TEKC/p1624558136437200 test.skip('When looking for chemicals affected by Phenotype Increased Urinary Glycerol, Glycerol should pop up', async () => { const queryHandler = new TRAPIQueryHandler.TRAPIQueryHandler({}, undefined, undefined, true); const query = JSON.parse( - fs.readFileSync(path.join(example_foler, 'increased_urinary_glycerol_affects_glycerol.json')), + fs.readFileSync(path.join(example_folder, 'increased_urinary_glycerol_affects_glycerol.json')), ); queryHandler.setQueryGraph(query.message.query_graph); await queryHandler.query(); @@ -18,12 +18,14 @@ describe('Testing TRAPIQueryHandler Module', () => { expect(res.message.knowledge_graph.nodes).toHaveProperty('CHEBI:17754'); }); - test('When looking for genes related to Disease DYSKINESIA, FAMILIAL, WITH FACIAL MYOKYMIA, ACDY5 should pop up', async () => { + // skip until integrity tests can be rewritten with mocked API responses + test.skip('When looking for genes related to Disease DYSKINESIA, FAMILIAL, WITH FACIAL MYOKYMIA, ACDY5 should pop up', async () => { const queryHandler = new TRAPIQueryHandler.TRAPIQueryHandler({}, undefined, undefined, true); - const query = JSON.parse(fs.readFileSync(path.join(example_foler, 'FDFM_caused_by_ACDY5.json'))); + const query = JSON.parse(fs.readFileSync(path.join(example_folder, 'FDFM_caused_by_ACDY5.json'))); queryHandler.setQueryGraph(query.message.query_graph); await queryHandler.query(); const res = queryHandler.getResponse(); + console.log(res); expect(res.message.knowledge_graph.nodes).toHaveProperty('NCBIGene:111'); }); @@ -31,7 +33,7 @@ describe('Testing TRAPIQueryHandler Module', () => { test.skip('When looking for chemicals targeting IL1 Signaling patway, curcumin should pop up', async () => { const queryHandler = new TRAPIQueryHandler.TRAPIQueryHandler({}, undefined, undefined, true); const query = JSON.parse( - fs.readFileSync(path.join(example_foler, 'chemicals_targeting_IL1_Signaling_Pathway.json')), + fs.readFileSync(path.join(example_folder, 'chemicals_targeting_IL1_Signaling_Pathway.json')), ); queryHandler.setQueryGraph(query.message.query_graph); await queryHandler.query(); From 7af396007612b2fb3d7220be71637d213acc8019 Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Wed, 14 Dec 2022 12:32:17 -0500 Subject: [PATCH 33/34] feat: qualifier-filter for creative mode template matching --- src/inferred_mode/inferred_mode.js | 33 +++++++++++++++++++--------- src/inferred_mode/template_lookup.js | 5 ++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/inferred_mode/inferred_mode.js b/src/inferred_mode/inferred_mode.js index 51d6271f..0f7450bd 100644 --- a/src/inferred_mode/inferred_mode.js +++ b/src/inferred_mode/inferred_mode.js @@ -106,18 +106,31 @@ module.exports = class InferredQueryHandler { const expandedObject = qObject.categories.reduce((arr, objectCategory) => { return utils.getUnique([...arr, ...biolink.getDescendantClasses(utils.removeBioLinkPrefix(objectCategory))]); }, []); + const qualifierConstraints = (qEdge.qualifier_constraints || []).map((qualifierSetObj) => { + return Object.fromEntries( + qualifierSetObj.qualifier_set.map(({ qualifier_type_id, qualifier_value }) => [ + qualifier_type_id.replace('biolink:', ''), + qualifier_value.replace('biolink:', ''), + ]), + ); + }); + if (qualifierConstraints.length === 0) qualifierConstraints.push({}); const lookupObjects = expandedSubject.reduce((arr, subjectCategory) => { let templates = expandedObject.reduce((arr2, objectCategory) => { - return [ - ...arr2, - ...expandedPredicates.map((predicate) => { - return { - subject: utils.removeBioLinkPrefix(subjectCategory), - object: utils.removeBioLinkPrefix(objectCategory), - predicate: utils.removeBioLinkPrefix(predicate), - }; - }), - ]; + let templates2 = qualifierConstraints.reduce((arr3, qualifierSet) => { + return [ + ...arr3, + ...expandedPredicates.map((predicate) => { + return { + subject: utils.removeBioLinkPrefix(subjectCategory), + object: utils.removeBioLinkPrefix(objectCategory), + predicate: utils.removeBioLinkPrefix(predicate), + qualifiers: qualifierSet, + }; + }), + ]; + }, []); + return [...arr2, ...templates2]; }, []); return [...arr, ...templates]; }, []); diff --git a/src/inferred_mode/template_lookup.js b/src/inferred_mode/template_lookup.js index e44fed60..39ad6beb 100644 --- a/src/inferred_mode/template_lookup.js +++ b/src/inferred_mode/template_lookup.js @@ -26,7 +26,10 @@ exports.getTemplates = async (lookups) => { return ( group.subject.includes(lookup.subject) && group.object.includes(lookup.object) && - group.predicate.includes(lookup.predicate) + group.predicate.includes(lookup.predicate) && + Object.entries(lookup.qualifiers || {}).every(([qualifierType, qualifierValue]) => { + return (group.qualifiers || {})[qualifierType] && group.qualifiers[qualifierType] === qualifierValue; + }) ); }); From e51bb36e5b786a22d6706ed001cd9867d00ff63c Mon Sep 17 00:00:00 2001 From: tokebe <43009413+tokebe@users.noreply.github.com> Date: Wed, 21 Dec 2022 11:35:33 -0500 Subject: [PATCH 34/34] perf: node category expansion, curie intersection code --- src/query_node.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/query_node.js b/src/query_node.js index a3f04e95..4c00b33a 100644 --- a/src/query_node.js +++ b/src/query_node.js @@ -12,6 +12,8 @@ module.exports = class QNode { constructor(info) { this.id = info.id; this.category = info.categories || 'NamedThing'; + this.expandedCategories = this.category; + this.equivalentIDsUpdated = false; // mainIDs this.curie = info.ids; //is_set @@ -31,6 +33,7 @@ module.exports = class QNode { //object-ify array of initial curies if (info.expanded_curie === undefined) this.expandCurie(); this.validateConstraints(); + this.expandCategories(); } freeze() { @@ -140,18 +143,16 @@ module.exports = class QNode { intersectWithExpandedCuries(newCuries) { let keep = {}; - for (const mainID in newCuries) { - let current_list_of_aliases = newCuries[mainID]; - for (const existingMainID in this.expanded_curie) { - let existing_list_of_aliases = this.expanded_curie[existingMainID]; - let idsMatchFound = _.intersection(current_list_of_aliases, existing_list_of_aliases); - if (idsMatchFound.length) { - if (!Object.hasOwnProperty.call(keep, mainID)) { - keep[mainID] = current_list_of_aliases; - } - } + // If a new entity has any alias intersection with an existing entity, keep it + Object.entries(newCuries).forEach(([newMainID, currentAliases]) => { + const someIntersection = Object.entries(this.expanded_curie).some(([existingMainID, existingAliases]) => { + return currentAliases.some((currentAlias) => existingAliases.includes(currentAlias)); + }); + if (someIntersection) { + if (!keep[newMainID]) keep[newMainID] = currentAliases; } - } + }); + //save expanded curies (main + aliases) this.expanded_curie = keep; //save curies (main ids) @@ -183,6 +184,12 @@ module.exports = class QNode { } getCategories() { + if (this.equivalentIDsUpdated) this.expandCategories(); + return this.expandedCategories; + } + + expandCategories() { + this.equivalentIDsUpdated = false; if (this.hasEquivalentIDs() === false) { const categories = utils.toArray(this.category); let expanded_categories = []; @@ -192,7 +199,8 @@ module.exports = class QNode { ...(biolink.getDescendantClasses(utils.removeBioLinkPrefix(category)) || []), ]; }); - return utils.getUnique(expanded_categories); + this.expandedCategories = utils.getUnique(expanded_categories); + return; } // let ancestors = new Set( // utils @@ -207,7 +215,7 @@ module.exports = class QNode { categories = [...categories, entity.semanticType]; }); }); - return utils.getUnique( + this.expandedCategories = utils.getUnique( utils.getUnique(categories).reduce((arr, category) => [...arr, ...(biolink.getDescendantClasses(category) || [])], []), ); // .filter(category => !ancestors.has(category)); @@ -225,6 +233,7 @@ module.exports = class QNode { setEquivalentIDs(equivalentIDs) { this.equivalentIDs = equivalentIDs; + this.equivalentIDsUpdated = true; } updateEquivalentIDs(equivalentIDs) { @@ -233,6 +242,7 @@ module.exports = class QNode { } else { this.equivalentIDs = { ...this.equivalentIDs, ...equivalentIDs }; } + this.equivalentIDsUpdated = true; } hasInput() {