diff --git a/package-lock.json b/package-lock.json index 3b196da0..1ced7b2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bcgsc-pori/graphkb-loader", - "version": "6.3.3", + "version": "6.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bcgsc-pori/graphkb-loader", - "version": "6.3.3", + "version": "6.4.0", "license": "GPL-3", "dependencies": { "@bcgsc-pori/graphkb-parser": "^1.1.1", diff --git a/package.json b/package.json index 7f738792..de1583e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@bcgsc-pori/graphkb-loader", "main": "src/index.js", - "version": "6.3.3", + "version": "6.4.0", "repository": { "type": "git", "url": "https://github.com/bcgsc/pori_graphkb_loader.git" @@ -64,4 +64,4 @@ "start:signatures": "node bin/load.js file ontology data/signatures.json", "start:vocabulary": "node bin/load.js file ontology data/vocab.json" } -} \ No newline at end of file +} diff --git a/src/civic/evidenceItems.graphql b/src/civic/evidenceItems.graphql index 1eb20289..1c6158cf 100644 --- a/src/civic/evidenceItems.graphql +++ b/src/civic/evidenceItems.graphql @@ -71,6 +71,11 @@ query evidenceItems( molecularProfile { id name + parsedName { + ... on Gene { entrezId } + ... on MolecularProfileTextSegment { text } + ... on Variant { id } + } rawName variants { gene { diff --git a/src/civic/index.js b/src/civic/index.js index dee77cf5..bf78bef1 100644 --- a/src/civic/index.js +++ b/src/civic/index.js @@ -21,6 +21,7 @@ const { civic: SOURCE_DEFN, ncit: NCIT_SOURCE_DEFN } = require('../sources'); const { processVariantRecord } = require('./variant'); const { getRelevance } = require('./relevance'); const { getPublication } = require('./publication'); +const { MolecularProfile } = require('./profile'); const { EvidenceItem: evidenceSpec } = require('./specs.json'); class NotImplementedError extends ErrorMixin { } @@ -126,7 +127,7 @@ const getTherapy = async (conn, therapyRecord) => { }; -/** * +/** * Add or fetch a therapy combination if there is not an existing record * Link the therapy combination to its individual elements */ @@ -161,6 +162,9 @@ const addOrFetchTherapy = async (conn, source, therapiesRecords, combinationType }; +/** + * Add or fetch an evidence level if there is not an existing record + */ const getEvidenceLevel = async ({ conn, rawRecord, sources, }) => { @@ -309,6 +313,10 @@ const processEvidenceRecord = async (opt) => { content.subject = rid(await conn.getVocabularyTerm('patient')); } if (rawRecord.evidenceType === 'FUNCTIONAL') { content.subject = rid(feature); + } if (rawRecord.evidenceType === 'ONCOGENIC') { + content.subject = variants.length === 1 + ? rid(variants[0]) + : rid(feature); } if (content.subject && !content.conditions.includes(content.subject)) { @@ -405,13 +413,13 @@ const fetchDeletedEvidenceItems = async (url) => { logger.info(`loading rejected evidenceItems from ${url}`); const rejected = await requestEvidenceItems(url, { query: `query evidenceItems($after: String, $status: EvidenceStatusFilter) { - evidenceItems(after: $after, status: $status) { - nodes {id} - pageCount - pageInfo {endCursor, hasNextPage} - totalCount - } - }`, + evidenceItems(after: $after, status: $status) { + nodes {id} + pageCount + pageInfo {endCursor, hasNextPage} + totalCount + } + }`, variables: { status: 'REJECTED', }, @@ -476,46 +484,12 @@ const downloadEvidenceRecords = async (url, trustedCurators) => { counts.error++; continue; } - - if ( - record.significance === 'NA' - || (record.significance === null && record.evidenceType === 'PREDICTIVE') - ) { - counts.skip++; - logger.debug(`skipping uninformative record (${record.id})`); - } else { - records.push(record); - } + records.push(record); } return { counts, errorList, records }; }; -/** - * Splits a variant into a list of it's variations - * Desambiguate variants that as been linked as "or" - * Ex. {name: 'Q157P/R'} --> [{name: 'Q157P'}, {name: 'Q157R'}] - * Ex. {name: 'Q157P'} --> [{name: 'Q157P'}] - * - * @param {Object} variant the Variant object to desambigaute - * @returns {Object[]} an array of Variant objects - */ -const disambiguateVariant = (variant) => { - let variants = [variant], - orCombination; - - if (orCombination = /^([a-z]\d+)([a-z])\/([a-z])$/i.exec(variant.name)) { - const [, prefix, tail1, tail2] = orCombination; - variants = [ - { ...variant, name: `${prefix}${tail1}` }, - { ...variant, name: `${prefix}${tail2}` }, - ]; - } - - return variants; -}; - - /** * Access the CIVic API, parse content, transform and load into GraphKB * @@ -553,9 +527,8 @@ const upload = async ({ records: {}, }; - // Refactor records into recordsById and varById + // Refactor records into recordsById const recordsById = {}; - const varById = {}; for (const record of records) { // Check if max records limit has been reached @@ -566,25 +539,22 @@ const upload = async ({ // Check if record id is unique if (recordsById[record.id]) { - logger.warn(`Multiple evidenceItems with the same id: ${record.id}. Violates assumptions. Only the 1st one was kept.`); + logger.error(`Multiple evidenceItems with the same id: ${record.id}. Violates assumptions. Only the 1st one was kept.`); + counts.skip++; continue; } // Introducing Molecular Profiles with CIViC GraphQL API v2.2.0 // [EvidenceItem]--(many-to-one)--[MolecularProfile]--(many-to-many)--[Variant] - if (record.molecularProfile && record.molecularProfile.id) { - if (record.molecularProfile.variants.length === 0) { - throw new Error(`Molecular Profile without Variant. Violates assumptions: ${record.molecularProfile.id}`); - } else if (record.molecularProfile.variants.length > 1) { - // TODO: Add support for Evidence Item with complex Molecular Profile - logger.warn(`Skip upload of Evidence Item with complex Molecular Profile (those with more that 1 Variant): ${record.id}`); - continue; - } else { - // Assuming 1 Variant per Molecular Profile - varById[record.molecularProfile.variants[0].id.toString()] = record.molecularProfile.variants[0]; - } - } else { - throw new Error(`Evidence Item without Molecular Profile. Violates assumptions: ${record.id}`); + if (!record.molecularProfile) { + logger.error(`Evidence Item without Molecular Profile. Violates assumptions: ${record.id}`); + counts.skip++; + continue; + } + if (!record.molecularProfile.variants || record.molecularProfile.variants.length === 0) { + logger.error(`Molecular Profile without Variants. Violates assumptions: ${record.molecularProfile.id}`); + counts.skip++; + continue; } // Adding EvidenceItem to object for upload @@ -626,46 +596,55 @@ const upload = async ({ continue; } - // Variant disambiguation - // Assuming 1 Variant per Molecular Profile - record.variants = disambiguateVariant(varById[record.molecularProfile.variants[0].id.toString()]); + // Process Molecular Profiles expression into an array of conditions + // Each condition is itself an array of variants, one array for each expected GraphKB Statement from this CIViC Evidence Item + const Mp = MolecularProfile(record.molecularProfile); + try { + record.conditions = Mp.process().conditions; + } catch (err) { + logger.error(`evidence (${record.id}) ${err}`); + counts.skip += 1; + continue; + } - const oneToOne = (record.variants.length * record.therapies.length) === 1 && preupload.size === 1; const postupload = []; // Upload all GraphKB statements for this CIViC Evidence Item - for (const variant of record.variants) { - for (const therapies of record.therapies) { - try { - logger.debug(`processing ${record.id}`); - const result = await processEvidenceRecord({ - conn, - oneToOne, - rawRecord: { ..._.omit(record, ['therapies', 'variants']), therapies, variant }, - sources: { civic: source }, - variantsCache, - }); - postupload.push(rid(result)); - counts.success += 1; - } catch (err) { - if ( - err.toString().includes('is not a function') - || err.toString().includes('of undefined') - ) { - console.error(err); + for (const condition of record.conditions) { + const oneToOne = (condition.length * record.therapies.length) === 1 && preupload.size === 1; + + for (const variant of condition) { + for (const therapies of record.therapies) { + try { + logger.debug(`processing ${record.id}`); + const result = await processEvidenceRecord({ + conn, + oneToOne, + rawRecord: { ..._.omit(record, ['therapies', 'variants']), therapies, variant }, + sources: { civic: source }, + variantsCache, + }); + postupload.push(rid(result)); + counts.success += 1; + } catch (err) { + if ( + err.toString().includes('is not a function') + || err.toString().includes('of undefined') + ) { + console.error(err); + } + if (err instanceof NotImplementedError) { + // accepted evidence that we do not support loading. Should delete as it may have changed from something we did support + purgeableEvidenceItems.add(sourceId); + } + errorList.push({ error: err, errorMessage: err.toString(), record }); + logger.error(`evidence (${record.id}) ${err}`); + counts.error += 1; } - if (err instanceof NotImplementedError) { - // accepted evidence that we do not support loading. Should delete as it may have changed from something we did support - purgeableEvidenceItems.add(sourceId); - } - errorList.push({ error: err, errorMessage: err.toString(), record }); - logger.error(`evidence (${record.id}) ${err}`); - counts.error += 1; } } } - // compare statments before/after upload to determine if any records should be soft-deleted postupload.forEach((id) => { preupload.delete(id); @@ -673,8 +652,8 @@ const upload = async ({ if (preupload.size && purgeableEvidenceItems.has(sourceId)) { logger.warn(` - Removing ${preupload.size} CIViC Entries (EID:${sourceId}) of unsupported format - `); + Removing ${preupload.size} CIViC Entries (EID:${sourceId}) of unsupported format + `); try { await Promise.all( @@ -733,7 +712,6 @@ const upload = async ({ module.exports = { SOURCE_DEFN, - disambiguateVariant, specs: { validateEvidenceSpec }, upload, }; diff --git a/src/civic/profile.js b/src/civic/profile.js new file mode 100644 index 00000000..00222b1d --- /dev/null +++ b/src/civic/profile.js @@ -0,0 +1,247 @@ +const { error: { ErrorMixin } } = require('@bcgsc-pori/graphkb-parser'); + +class NotImplementedError extends ErrorMixin { } + + +/** + * Factory function returning a MolecularProfile object. + * The process() method allows for the process of Civic's Molecular Profiles + * After processing, the conditions property stores an array of GraphKB Statement's conditions + * + * @param {Object} molecularProfile a Molecular Profile segment from GraphQL query + * @returns {MolecularProfile} object whose conditions' property is an array of lists of conditions + */ +const MolecularProfile = (molecularProfile) => ({ + /* Combine new conditional variants with existing conditions */ + _combine({ arr1, arr2 }) { + const combinations = []; + + if (arr1[0].length === 0) { + return arr2; + } + if (arr2[0].length === 0) { + return arr1; + } + + arr1.forEach((e1) => { + arr2.forEach((e2) => { + e2.forEach((variant) => { + combinations.push([...e1, variant]); + }); + }); + }); + return combinations; + }, + /* Compile parsed block into array of conditions' arrays */ + _compile({ arr, op, part }) { + let conditions = []; + + switch (op) { + case 'AND': + arr.forEach((arrEl) => { + part.forEach((partEl) => { + conditions.push([...arrEl, ...partEl]); + }); + }); + break; + + case 'OR': + if (arr[0].length === 0) { + conditions = [...part]; + } else { + conditions = [...arr, ...part]; + } + break; + + default: + break; + } + return conditions; + }, + /* Desambiguation of variants with implicit 'or' in the name */ + _disambiguate() { + // Split ambiguous variants + const temp = []; + this.conditions.forEach((condition) => { + condition.forEach((variant) => { + temp.push( + this._split(variant), + ); + }); + }); + + let newCondition; + + // Combine variations into new condition + for (let i = 0; i < temp.length; i++) { + newCondition = this._combine({ arr1: newCondition || [[]], arr2: temp[i] }); + } + this.conditions = newCondition; + return this; + }, + /* Returns index of closing parenthesis for end of block */ + _end({ block, i, offset }) { + let count = 1, + j = 0; + + while (count > 0) { + j++; + + if (block[i + offset + j].text) { + switch (block[i + offset + j].text) { + case '(': + count++; + break; + + case ')': + count--; + break; + + default: + break; + } + } + } + return j; + }, + /* Returns true if parsedName contains NOT operator(s), otherwise false */ + _not(parsedName) { + for (let i = 0; i < parsedName.length; i++) { + if (parsedName[i].text && parsedName[i].text === 'NOT') { + return true; + } + } + return false; + }, + /* Parse block expression into array of conditions' arrays */ + _parse(block) { + let conditions = [[]], + offset = 0, + op = 'OR'; // Default operator + + for (let i = 0; i + offset < block.length; i++) { + const idx = i + offset; + + // If Variant + if (block[idx].id) { + // Add variant as a condition + conditions = this._compile({ + arr: conditions, + op, + part: [[block[idx].id]], + }); + continue; + } + + // If Nested block + if (block[idx].text && block[idx].text === '(') { + // Get end of block' index + const j = this._end({ + block, + i, + offset, + }); + // Recursively parse nested block + conditions = this._compile({ + arr: conditions, + op, + part: this._parse(block.slice(idx + 1, idx + j)), + }); + // New offset for rest of current block + offset += j; + continue; + } + + // If Operator + if (block[idx].text && ['AND', 'OR'].includes(block[idx].text)) { + op = block[idx].text; + continue; + } + } + return conditions; + }, + /* Splits variant's object into it's variations + * Ex. {name: 'Q157P/R'} --> [[ {name: 'Q157P'} ], [ {name: 'Q157R'} ]] */ + _split(variant) { + let orCombination; + + if (orCombination = /^([a-z]\d+)([a-z])\/([a-z])$/i.exec(variant.name)) { + const [, prefix, tail1, tail2] = orCombination; + return [ + [{ ...variant, name: `${prefix}${tail1}` }], + [{ ...variant, name: `${prefix}${tail2}` }], + ]; + } + return [[variant]]; + }, + /* Convert variant ids to variant objects */ + _variants() { + // Getting variants by ids from molecular profile's variants array + const variantsById = {}; + this.profile.variants.forEach((variant) => { + variantsById[variant.id] = variant; + }); + + // Refactoring conditions with variant objects + const newConditions = []; + this.conditions.forEach((condition) => { + newConditions.push(condition.map(id => variantsById[id])); + }); + + // Check if any missing variant object + newConditions.forEach((condition) => { + condition.forEach((variant) => { + if (!variant) { + throw new Error( + `unable to process molecular profile with missing or misformatted variants (${this.profile.id || ''})`, + ); + } + }); + }); + + // Replacing conditions with ones with variant's objects + this.conditions = newConditions; + return this; + }, + /* Corresponding GKB Statements' conditions (1 array per statement) */ + conditions: [[]], + /* Main object's method. Process expression into array of conditions' arrays */ + process() { + // Get Molecular Profile's expression (parsedName property) + const { parsedName } = this.profile; + + // Check for expression's format + if (!parsedName + || !Array.isArray(parsedName) + || parsedName.length === 0 + || typeof parsedName[0] !== 'object' + ) { + throw new Error( + `unable to process molecular profile with missing or misformatted parsedName (${this.profile.id || ''})`, + ); + } + // NOT operator not yet supported + if (this._not(parsedName)) { + throw new NotImplementedError( + `unable to process molecular profile with NOT operator (${this.profile.id || ''})`, + ); + } + // Filters out unwanted gene's info from expression + const filteredParsedName = parsedName.filter(el => !el.entrezId); + + // Parse expression into conditions + this.conditions = this._parse(filteredParsedName); + // Replace Variant's ids with corresponding Variant's objects + this._variants(); + // Disambiguate Variants' names with implicit 'or' + this._disambiguate(); + return this; + }, + /* CIViC Evidence Item's Molecular Profile segment */ + profile: molecularProfile || {}, +}); + + +module.exports = { + MolecularProfile, +}; diff --git a/src/civic/specs.json b/src/civic/specs.json index a7039bf2..2a087ac1 100644 --- a/src/civic/specs.json +++ b/src/civic/specs.json @@ -82,6 +82,37 @@ "string" ] }, + "parsedName": { + "items": { + "anyOf": [ + { + "properties": { + "entrezId": { + "type": "number" + } + }, + "type": "object" + }, + { + "properties": { + "id": { + "type": "number" + } + }, + "type": "object" + }, + { + "properties": { + "text": { + "type": "string" + } + }, + "type": "object" + } + ] + }, + "type": "array" + }, "rawName": { "type": [ "null", diff --git a/test/civic.profile.test.js b/test/civic.profile.test.js new file mode 100644 index 00000000..9702e98b --- /dev/null +++ b/test/civic.profile.test.js @@ -0,0 +1,226 @@ +const { MolecularProfile } = require('../src/civic/profile'); + + +describe('MolecularProfile._combine()', () => { + test.each([ + [{ arr1: [[]], arr2: [[]] }, [[]]], + [{ arr1: [['A']], arr2: [[]] }, [['A']]], + [{ arr1: [[]], arr2: [['B']] }, [['B']]], + [{ arr1: [['A']], arr2: [['B']] }, [['A', 'B']]], + [{ arr1: [['A']], arr2: [['B'], ['C']] }, [['A', 'B'], ['A', 'C']]], + [{ arr1: [['A'], ['B']], arr2: [['C'], ['D']] }, [['A', 'C'], ['A', 'D'], ['B', 'C'], ['B', 'D']]], + ])( + 'combine some conditions', ({ arr1, arr2 }, expected) => { + expect(MolecularProfile()._combine({ arr1, arr2 })).toEqual(expected); + }, + ); +}); + +describe('MolecularProfile._compile()', () => { + test.each([ + [[['A', 'B']], 'AND', [['C', 'D']], [['A', 'B', 'C', 'D']]], + [[['A', 'B']], 'AND', [['C', 'D'], ['E', 'F']], [['A', 'B', 'C', 'D'], ['A', 'B', 'E', 'F']]], + [[['A', 'B'], ['C', 'D']], 'AND', [['E', 'F']], [['A', 'B', 'E', 'F'], ['C', 'D', 'E', 'F']]], + [[['A', 'B']], 'OR', [['C', 'D']], [['A', 'B'], ['C', 'D']]], + [[['A', 'B']], 'OR', [['C', 'D'], ['E', 'F']], [['A', 'B'], ['C', 'D'], ['E', 'F']]], + [[['A', 'B'], ['C', 'D']], 'OR', [['E', 'F']], [['A', 'B'], ['C', 'D'], ['E', 'F']]], + ])( + 'compile somme expressions', (arr, op, part, expected) => { + expect(MolecularProfile()._compile({ arr, op, part })).toEqual(expected); + }, + ); +}); + +describe('MolecularProfile._disambiguate()', () => { + test('disambiguate conditions', () => { + const Mp = MolecularProfile(); + Mp.conditions = [ + [{ id: 8, name: 'X123M/N' }, { id: 9, name: 'X456O/P' }, { id: 10, name: 'X456Q' }], + ]; + expect(Mp._disambiguate().conditions).toEqual( + [ + [{ id: 8, name: 'X123M' }, { id: 9, name: 'X456O' }, { id: 10, name: 'X456Q' }], + [{ id: 8, name: 'X123M' }, { id: 9, name: 'X456P' }, { id: 10, name: 'X456Q' }], + [{ id: 8, name: 'X123N' }, { id: 9, name: 'X456O' }, { id: 10, name: 'X456Q' }], + [{ id: 8, name: 'X123N' }, { id: 9, name: 'X456P' }, { id: 10, name: 'X456Q' }], + ], + ); + }); +}); + +describe('MolecularProfile._end()', () => { + const block = [ + { id: 1 }, { text: 'AND' }, + { text: '(' }, { id: 2 }, { text: 'OR' }, { id: 3 }, { text: ')' }, { text: 'AND' }, + { text: '(' }, { id: 4 }, { text: 'OR' }, + { text: '(' }, { id: 5 }, { text: 'AND' }, { id: 6 }, { text: ')' }, { text: ')' }, + ]; + + test.each([ + [2, 0, 4], + [4, 4, 8], + [6, 4, 6], + ])( + 'testing index and offset combinations: i=%s, offset=%s', (i, offset, expected) => { + expect(MolecularProfile()._end({ block, i, offset })).toEqual(expected); + }, + ); +}); + +describe('MolecularProfile._not()', () => { + test('check for presence of NOT operator in expression', () => { + expect(MolecularProfile()._not([ + { entrezId: 9 }, { id: 1 }, { text: 'AND' }, { text: 'NOT' }, { text: '(' }, + { entrezId: 9 }, { id: 2 }, { text: 'OR' }, { entrezId: 9 }, { id: 3 }, { text: ')' }, + ])).toBe(true); + expect(MolecularProfile()._not([ + { entrezId: 9 }, { id: 1 }, { text: 'AND' }, { text: '(' }, { entrezId: 9 }, + { id: 2 }, { text: 'OR' }, { entrezId: 9 }, { id: 3 }, { text: ')' }, + ])).toBe(false); + }); +}); + +describe('MolecularProfile._parse()', () => { + test.each([ + [ + [{ id: 1 }, { text: 'AND' }, { id: 2 }], + [[1, 2]], + ], + [ + [{ id: 1 }, { text: 'OR' }, { id: 2 }], + [[1], [2]], + ], + [ + [{ id: 1 }, { text: 'AND' }, { text: '(' }, { id: 2 }, { text: 'OR' }, { id: 3 }, { text: ')' }], + [[1, 2], [1, 3]], + ], + [ + [{ id: 1 }, { text: 'OR' }, { text: '(' }, { id: 2 }, { text: 'AND' }, { id: 3 }, { text: ')' }], + [[1], [2, 3]], + ], + [ + [ + { text: '(' }, { id: 1 }, { text: 'AND' }, { id: 2 }, { text: ')' }, + { text: 'OR' }, { text: '(' }, { id: 3 }, { text: 'AND' }, { id: 4 }, { text: ')' }, + ], + [[1, 2], [3, 4]], + ], + [ + [ + { text: '(' }, { id: 1 }, { text: 'OR' }, { id: 2 }, { text: ')' }, + { text: 'AND' }, { text: '(' }, { id: 3 }, { text: 'OR' }, { id: 4 }, { text: ')' }, + ], + [[1, 3], [1, 4], [2, 3], [2, 4]], + ], + [ + [ + { id: 1 }, { text: 'AND' }, { text: '(' }, { id: 2 }, { text: 'OR' }, { id: 3 }, { text: ')' }, + { text: 'AND' }, { text: '(' }, { id: 4 }, { text: 'OR' }, { id: 5 }, { text: ')' }, + ], + [[1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3, 5]], + ], + [ + [ + { id: 1 }, { text: 'OR' }, { text: '(' }, { id: 2 }, { text: 'AND' }, { id: 3 }, { text: ')' }, + { text: 'OR' }, { text: '(' }, { id: 4 }, { text: 'AND' }, { id: 5 }, { text: ')' }, + ], + [[1], [2, 3], [4, 5]], + ], + [ + [ + { id: 1 }, { text: 'AND' }, { text: '(' }, { id: 2 }, { text: 'AND' }, + { text: '(' }, { id: 3 }, { text: 'OR' }, { id: 4 }, { text: ')' }, { text: ')' }, + ], + [[1, 2, 3], [1, 2, 4]], + ], + ])( + 'testing some Molecular Profiles expressions', (block, expected) => { + expect(MolecularProfile()._parse(block)).toEqual(expected); + }, + ); +}); + + +describe('MolecularProfile._split()', () => { + test.each([ + ['Q157P/R', [[{ name: 'Q157P' }], [{ name: 'Q157R' }]]], + ['Q157P', [[{ name: 'Q157P' }]]], + ])( + 'Split %s into its variations', (name, expected) => { + expect(MolecularProfile()._split({ name })).toEqual(expected); + }, + ); +}); + +describe('MolecularProfile._variants()', () => { + test('variants ids replaced by objects', () => { + const Mp = MolecularProfile({ + variants: [ + { id: 1, name: 'a1' }, + { id: 2, name: 'a2' }, + { id: 3, name: 'a3' }, + ], + }); + Mp.conditions = [[1, 2], [1, 3]]; + expect(Mp._variants().conditions).toEqual([ + [{ id: 1, name: 'a1' }, { id: 2, name: 'a2' }], + [{ id: 1, name: 'a1' }, { id: 3, name: 'a3' }], + ]); + }); + + test('tests cases that should throw an Error', () => { + const molecularProfile = { + id: 123, + variants: [ + { id: 1, name: 'a1' }, + { id: 2, name: 'a2' }, + ], + }; + const Mp = MolecularProfile(molecularProfile); + Mp.conditions = [[1, 2], [1, 3]]; + expect(() => Mp._variants()).toThrow( + `unable to process molecular profile with missing or misformatted variants (${molecularProfile.id || ''})`, + ); + }); +}); + +describe('MolecularProfile.process()', () => { + test('gene infos not interfering', () => { + expect(MolecularProfile({ + parsedName: [{ entrezId: 9 }, { id: 1 }], + variants: [{ id: 1, name: 'a1' }], + }).process().conditions).toEqual([[{ id: 1, name: 'a1' }]]); + }); + + test.each([ + [{}], + [{ parsedName: '' }], + [{ parsedName: [] }], + [{ parsedName: [''] }], + ])( + 'tests cases that should throw an Error', (molecularProfile) => { + expect(() => MolecularProfile(molecularProfile).process()).toThrow( + `unable to process molecular profile with missing or misformatted parsedName (${molecularProfile.id || ''})`, + ); + }, + ); + + test('not providing a molecularProfile argument should also throw an Error', () => { + expect(() => MolecularProfile().process()).toThrow( + 'unable to process molecular profile with missing or misformatted parsedName ()', + ); + }); + + test('test case that should throw a NotImplementedError', () => { + const molecularProfile = { + id: 1, + parsedName: [ + { entrezId: 9 }, { id: 1 }, { text: 'AND' }, { text: 'NOT' }, { text: '(' }, + { entrezId: 9 }, { id: 2 }, { text: 'OR' }, { entrezId: 9 }, { id: 3 }, { text: ')' }, + ], + }; + expect(() => MolecularProfile(molecularProfile).process()).toThrow( + `unable to process molecular profile with NOT operator (${molecularProfile.id || ''})`, + ); + }); +});