diff --git a/.gitignore b/.gitignore index e6aca5b2ff4..bb1b073d818 100755 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ end-to-end-test/results-0-0..xml end-to-end-test/customReport.json end-to-end-test/customReportJSONP.js end-to-end-test/shared/results +api-e2e/json/merged-tests.json wdio/browserstack.txt env/custom.sh image-compare/errors.js @@ -65,3 +66,4 @@ e2e-localdb-workspace/ junit.xml .nvim.lua .luarc.json +api-e2e/validation.js diff --git a/api-e2e/clickhouse.js b/api-e2e/clickhouse.js new file mode 100644 index 00000000000..7e2ae93b5c7 --- /dev/null +++ b/api-e2e/clickhouse.js @@ -0,0 +1,229 @@ +const { createClient } = require('@clickhouse/client'); +const _ = require('lodash'); + +const client = createClient({ + host: + process.env.CLICKHOUSE_HOST ?? + 'https://mecgt250i0.us-east-1.aws.clickhouse.cloud:8443/cgds_public_v5', + username: process.env.CLICKHOUSE_USER ?? 'app_user', + password: process.env.CLICKHOUSE_PASSWORD ?? 'P@ssword1976', + request_timeout: 600000, +}); + +async function mainInsert() { + const studies = await getStudies(); + + const entities = await getEntities(); + + let profiles = await getProfiles(); + + const then = Date.now(); + + //profiles = profiles.slice(0,1); + + //console.log(profiles); + + for (const profile of profiles) { + console.log(`inserting ${profile.stable_id}`); + try { + await insertProfile(profile.genetic_profile_id); + } catch (ex) { + console.log(`ERROR inserting ${profile.stable_id}`); + console.log(ex); + } + } + + // for (const study of studies) { + // console.log( + // `inserting ${study.cancer_study_id} ${study.cancer_study_identifier}` + // ); + // try { + // await insertStudy(study.cancer_study_id); + // } catch (ex) { + // console.log( + // `ERROR inserting ${study.cancer_study_id} ${study.cancer_study_identifier}` + // ); + // console.log(ex); + // } + // } + + // for (const entity of entities) { + // try { + // await insertProfile(2071, entity.genetic_entity_id); + // } catch (ex) { + // console.log(`ERROR inserting ${entity.genetic_entity_id}`); + // console.log(ex); + // } + // } + + console.log(`Finished in ${Date.now() - then}`); + + //const result = insertStudy(392); + + //console.log(result); +} + +async function mainPatient() { + getPatientIds(); +} + +async function insertStudy(cancer_study_id) { + const resultSet = await client.query({ + query: queryByStudies({ cancer_study_id }), + format: 'JSONEachRow', + }); + + const dataset = await resultSet.json(); // or `row.text` to avoid parsing JSON + + return dataset; +} + +async function insertProfile(genetic_profile_id) { + const resultSet = await client.query({ + query: queryByProfiles({ genetic_profile_id }), + format: 'JSONEachRow', + }); + + const dataset = await resultSet.json(); // or `row.text` to avoid parsing JSON + + return dataset; +} + +async function getProfiles() { + const resultSet = await client.query({ + query: `SELECT * FROM genetic_profile + WHERE genetic_profile.genetic_alteration_type NOT IN ('GENERIC_ASSAY', 'MUTATION_EXTENDED', 'STRUCTURAL_VARIANT') + `, + format: 'JSONEachRow', + }); + + const dataset = await resultSet.json(); // or `row.text` to avoid parsing JSON + + return dataset; +} + +async function getEntities() { + const resultSet = await client.query({ + query: + 'SELECT * FROM genetic_alteration ga WHERE ga.genetic_profile_id=2071', + format: 'JSONEachRow', + }); + + const dataset = await resultSet.json(); // or `row.text` to avoid parsing JSON + + return dataset; +} + +async function getPatientIds() { + const resultSet = await client.query({ + query: `SELECT p.stable_id, cs.cancer_study_identifier FROM patient p + JOIN cancer_study cs ON cs.cancer_study_id = p.cancer_study_id + + `, + format: 'JSONEachRow', + }); + + const dataset = await resultSet.json(); // or `row.text` to avoid parsing JSON + + const grouped = _.groupBy( + dataset, + p => + `${p.stable_id.replace( + /-/g, + '' + )}|${p.cancer_study_identifier.toUpperCase()}` + ); + + console.log(_.values(grouped).length); + + console.log(_.values(grouped).filter(a => a.length > 1)); + + //return dataset; +} + +async function getStudies() { + const resultSet = await client.query({ + query: 'select * from cancer_study', + format: 'JSONEachRow', + }); + + const dataset = await resultSet.json(); // or `row.text` to avoid parsing JSON + + return dataset; +} + +const queryByStudies = _.template( + ` +INSERT INTO TABLE genetic_alteration_derivedFIXED +SELECT + sample_unique_id, + cancer_study_identifier, + hugo_gene_symbol, + replaceOne(stable_id, concat(sd.cancer_study_identifier, '_'), '') as profile_type, + alteration_value +FROM + (SELECT + sample_id, + hugo_gene_symbol, + stable_id, + alteration_value + FROM + (SELECT + g.hugo_gene_symbol AS hugo_gene_symbol, + gp.stable_id as stable_id, + arrayMap(x -> (x = '' ? NULL : x), splitByString(',', assumeNotNull(substring(ga.values, 1, length(ga.values) - 1)))) AS alteration_value, + arrayMap(x -> (x = '' ? NULL : toInt32(x)), splitByString(',', assumeNotNull(substring(gps.ordered_sample_list, 1, length(gps.ordered_sample_list) - 1)))) AS sample_id + FROM + genetic_profile gp + JOIN genetic_profile_samples gps ON gp.genetic_profile_id = gps.genetic_profile_id + JOIN genetic_alteration ga ON gp.genetic_profile_id = ga.genetic_profile_id + JOIN gene g ON ga.genetic_entity_id = g.genetic_entity_id + WHERE + cancer_study_id=<%= cancer_study_id %> + AND + gp.genetic_alteration_type NOT IN ('GENERIC_ASSAY', 'MUTATION_EXTENDED', 'STRUCTURAL_VARIANT') + ) + ARRAY JOIN alteration_value, sample_id + WHERE alteration_value != 'NA') AS subquery + JOIN sample_derived sd ON sd.internal_id = subquery.sample_id; + ` +); + +const queryByProfiles = _.template( + ` +INSERT INTO TABLE genetic_alteration_derivedFIXED +SELECT + sample_unique_id, + cancer_study_identifier, + hugo_gene_symbol, + replaceOne(stable_id, concat(sd.cancer_study_identifier, '_'), '') as profile_type, + alteration_value +FROM + (SELECT + sample_id, + hugo_gene_symbol, + stable_id, + alteration_value + FROM + (SELECT + g.hugo_gene_symbol AS hugo_gene_symbol, + gp.stable_id as stable_id, + arrayMap(x -> (x = '' ? NULL : x), splitByString(',', assumeNotNull(substring(ga.values, 1, length(ga.values) - 1)))) AS alteration_value, + arrayMap(x -> (x = '' ? NULL : toInt32(x)), splitByString(',', assumeNotNull(substring(gps.ordered_sample_list, 1, length(gps.ordered_sample_list) - 1)))) AS sample_id + FROM + genetic_profile gp + JOIN genetic_profile_samples gps ON gp.genetic_profile_id = gps.genetic_profile_id + JOIN genetic_alteration ga ON gp.genetic_profile_id = ga.genetic_profile_id + JOIN gene g ON ga.genetic_entity_id = g.genetic_entity_id + WHERE + ga.genetic_profile_id='<%= genetic_profile_id %>' + AND + gp.genetic_alteration_type NOT IN ('GENERIC_ASSAY', 'MUTATION_EXTENDED', 'STRUCTURAL_VARIANT') + ) + ARRAY JOIN alteration_value, sample_id + WHERE alteration_value != 'NA') AS subquery + JOIN sample_derived sd ON sd.internal_id = subquery.sample_id; + ` +); + +mainInsert(); diff --git a/api-e2e/json/nothing.json b/api-e2e/json/nothing.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api-e2e/mergeJson.js b/api-e2e/mergeJson.js new file mode 100644 index 00000000000..3a07ebc5e02 --- /dev/null +++ b/api-e2e/mergeJson.js @@ -0,0 +1,28 @@ +const fsProm = require('fs/promises'); +const path = require('path'); + +async function mergeApiTestJson() { + const files = (await fsProm.readdir('./apiTests/specs')).map(fileName => { + return path.join('./apiTests/specs', fileName); + }); + + const jsons = files.map(path => { + return fsProm.readFile(path).then(data => { + try { + const json = JSON.parse(data); + return { file: path, suites: json }; + } catch (ex) { + console.log('invalid apiTest json spec'); + return []; + } + }); + }); + + Promise.all(jsons) + .then(d => { + fsProm.writeFile('./apiTests/merged-tests.json', JSON.stringify(d)); + }) + .then(r => console.log('merged-tests.json written')); +} + +exports.mergeApiTestJson = mergeApiTestJson; diff --git a/api-e2e/merged-tests.json b/api-e2e/merged-tests.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/api-e2e/merged-tests.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/api-e2e/public.js b/api-e2e/public.js new file mode 100644 index 00000000000..ca8e2b3aeda --- /dev/null +++ b/api-e2e/public.js @@ -0,0 +1,250 @@ +const csv = require('csvtojson'); +let csvFilePath = './extract-2024-10-11T11_47_07.957Z.csv'; + +//csvFilePath = 'extract-2024-10-12T19_34_07.810Z.csv'; + +//csvFilePath = 'extract-2024-10-13T07_41_15.227Z.csv'; //BIG ONE + +csvFilePath = 'public/extract-2024-10-17T11_47_33.789Z.csv'; // 100k public + +// these are genie +//'genie/extract-2024-10-17T05_29_43.435Z.csv' passing as of 10/18 +//'genie/extract-2024-10-14T16_35_15.134Z.csv' passing as of 10/18 +//`genie/extract-2024-10-14T20_29_07.301Z.csv` passing as of 10/18 (20 supressed) +//'genie/extract-2024-10-14T20_29_07.301Z.csv' passing as of 10/18 (20 supressed) +//'genie/extract-2024-10-17T18_26_53.439Z.csv' 2 failures 10/18 -1413667366,767115642 (one is violin, other 1 missing mutation) + +csvFilePath = 'genie/extract-2024-10-17T18_26_53.439Z.csv'; // 'genie/extract-2024-10-14T20_29_07.301Z.csv'; + +const _ = require('lodash'); +const formatCurl = require('format-curl'); + +var axios = require('axios'); +var { runSpecs } = require('./validation'); + +var exclusions = []; + +const cliArgs = parseArgs(); + +const filters = []; + +let hashes = []; + +// these are for +// const moos = `-673406526,-2005371191,-517670758,-297044990,-1843047838,1806024553,-1144748981,-253195460,1612090123,1325352156,-969783118,-1532898703,-1252166631,-1970080199,1099177506,-208575755,111369452,-2105827951,1488562009,120267065,-292313552,836959411,-1704732196,-1555792159,-1320441691,1224210290,1270362153,10730406,-1899514961,288956321,-2113817139,-1413994933,-297626343,-1453024695,612255788,294196227,334389772,-390181133,1488680884,-2136697383,-1851989700,-355198818,-1309800115,152983341,397284593,-273143411,-1898646363,-1222068354,768740376,506650358,-1978121826,-400232064,-854603647,966120768,1595526137,82293504,1693730585,1103112135,-130897992,982465581,-546396458,202736822,200577571,-1458390145,-1842559442,2115230890,1689773109,1260579686,608735328,-897823386,-1572279128,-2124788882,-1395582859,100764153,-2082231907,955387813,-86796820,-1959670828,-98020122,1989522734,-1386302838,494569046,-1578480035,-1264908277,1158398385,-305101900,2048745056,-262932598,-850356102,-1792777678,1168983298,1411967402,1061979373,1035914715,180953340,1514094747,-1552113269,-1918506884,1008528915,-199052613,871245308,-1630089717,816765258,470830618,1442340714,590580569,-2008281110,455817433,-306067334,-301861134,732000609,-307109378,-961046683,1155220653,585398983,-1081770754,-46214137,-421841122,460740961,-1055177488,2062576853,616789385,913376734,1290285048,809096332,89467101,-310960451,-807914063,-485028167,-1534017795,-1167921147,936974935,-1555192267,-333794387,-2073853723,-505694523,-519244572,1142105423,-363435083,643026493,-1428593845,-1043873486,1977120,1146520892,-1587618066,202915433,-1756538928,-1203647954,1982015438,1113600170,1014223356,1291985665,-806867521,-1361531574,367815258,-1558930374,255383726,203998497,-1550855951,-1391593582,1610595048,1763472544,1853556760,-1839313216,649214677,899056493,1899934755,-670025579,1816938667,-5089684,260489931,1002976033,887308081,-1360456087,-7628346,1638022655,466748715,1107722551,1256401318,1251220339,-425181581,471868808,-337108571,-1825470371,308297064,-735124612,-1442353336,-1137827504,-1436856030,-349080261,225299066,-382489202,-1968597541`.split( +// ',' +// ); + +// these are for public 100k +const moos = `776162948,1791582317,1051626596,1090390722,1634096744,-1357626028,-517670758,-297044990,-723148861,-887269158,-157223238,-1609018360,688132288,-1012605014,663602026,1144436202,2080416722,2102636334,1616351403,116697458,-1145140218,440961037,1788201866,64435906,1526226372,954198833,-333224080,-77262384,-969783118,-2092699827,1122708088,705191799,-910997525,369597899,-589249907,-1733269776,532408117,793803824,2005451592,1946034863,1348899627,1736153850,2004861981,1069675380,-2104618878,-1375780045,-1436966818,-1498011539,-1840476236,1636322635,128823282,712950665,-1807144066,-760096379,1806024553,-1843047838,-1380244121,1908080601,2049138719,75325645,-1564433337,214093972,1584368526,739417518,788541298,-1388038023,-476428894,-1214473123,1798846884,-1336448229,-1479102524,1188628176,1211735112,267198007,-1042782005,1595526137,966120768,-1318775387,-770140034,458149073,-1320441691,1768280517,795362073,440551502,-1083874499,860411865,-1922049418,799514642,2103810746,344484572,-22512484,2075871805,1492814359,2086203791,1046746847,780773665,-1592145833,-1575752643,1528265113,-1374458187,-1474139020,582526768,-2074481817,-1628958227,-2005572217,701108624,-793717747,2078439121,903680661,604898834,1159409033,1376551148,-1025123895,406726936,-1451816705,844353247,-1086596952,-524277403,-1173492647,-263714217,-1961112625,173857178,810614460,776625487,-31077333,-407077673,1152625827,-1204029497,-89652514,950808923,1051361409,655075270,1268317673,-429080735,-180799065,-1918582159,1007754487,445434938,-925144313,-753993222,-828029157,80215185,422205512,-418276888,286347826,1427361749,1841103889,195201275,-278082425,808983848,-558350133,-1982884570,1793886130,104465905,2020005969,-707477776,-1978778776,-146997675,-1605436757,-1288011598,1892578715,-421733597,-2058737510,80797926,2011748125,643117475,656010203,-1838584313,-1379158149,751580838,-300376426,651701037,-284421827,1414588548,1578030439,-1507068659,-552803979,2003360028,1012087380,1119495128,-1018724775,721889439,-1386470488,264752805,-1135621441,53685031,1749320410,-936382059,-271726321,-162436654,-967499791,-924355432,-1967807980,790912582,104911974,105129966,-1015598432,-1547063844,766139610,186806395,1609106547,1137458771,-79793169,-366866963,403522477,1692625915,1918102498,1948188379,1638022655,-7628346,859917518,984138719,-1727165229,-909568546,-1384377258,-1102903634,651279273,-621859020,50108050,721556658,-971350762,55114441,710343427,-1000493817,2065459736,-2010309650,-642014706,1673252949,1288568484,-673406526,-2005371191,1763472544,-1839313216,584288761,899056493,1853556760,-1017785218,-375634338,-217771303,332837231,519954103,-1260921458,1770567231,-848340841,-1575059881,1702190015,1767257846,-90449809,617363688,2134508661,-1353337156,-553248058,1465520871,-2088091542,441238581,263322615,-1243420049,-90200341,533957831,1315006521,1178063322,166833031,-557402244,1128260157,-2090759291,1960991180,-2143298382,2061460930,540705850,-891152750,551129118,364406009,1221598853,788481188,305474268,1341878576,-1625668352,-778412359,-1155472560,463536766,1955600881,-1339515224,1010291232,-1923309873,1182161716,-1303867461,20329773,1116578757,1408885855,1550380971,-1325200994,-941248117,1182607929,-475796764,-1396246057,-93501061,317029493,-960086759,-614052829,-106281559,-1543588030,-490981905,-583187386,-1698981871,452337345,-1946046494,726060739,2076629381` + .split(',') + .map(h => new RegExp(h)); + +//hashes = moos.map(s => new RegExp(s)); + +if (cliArgs.h) { + hashes = cliArgs.h.split(',').map(h => new RegExp(h)); +} + +const START = cliArgs.s || 0; +const LIMIT = cliArgs.l || 10000000; + +async function main() { + const files = await csv() + .fromFile(csvFilePath) + .then(async jsonObj => { + // clean out errant leading single quote + jsonObj.forEach(d => (d['@hash'] = d['@hash'].replace(/^'/, ''))); + + let uniq = _.uniqBy(jsonObj, '@hash') + .filter(d => { + return _.every( + exclusions.map(re => re.test(d['@url']) === false) + ); + }) + .filter(d => { + return ( + filters.length === 0 || + _.every(filters.map(re => re.test(d['@url']) === true)) + ); + }) + .filter(d => { + return ( + hashes.length === 0 || + _.some(hashes.map(re => re.test(d['@hash']) === true)) + ); + }); + + const tests = uniq.slice(START, START + LIMIT).reduce((aggr, d) => { + //delete data.genomicProfiles; + //delete data.genomicDataFilters; + + try { + let data = JSON.parse(d['@data']); + + const url = d['@url'] + .replace(/^"|"$/g, '') + .replace(/^\/\/[^\/]*/, '') + .replace(/\/api\//, '/api/column-store/'); + + const label = d['@url'] + .match(/\/api\/[^\/]*/i)[0] + .replace(/\/api\//, '') + .split('-') + .map(s => s.replace(/^./, ss => ss.toUpperCase())) + .join(''); + + aggr.push({ + hash: d['@hash'], + label, + data, + url, + }); + } catch (err) { + console.log(err); + } + return aggr; + }, []); + + // tests.forEach((t)=>{ + // console.log(t.data.studyIds || t.data.studyViewFilter.studyIds); + // }) + + const fakeFiles = [ + { + file: 'fake', + suites: [ + { + tests, + }, + ], + }, + ]; + + return fakeFiles; + }); + + runSpecs( + files, + axios, + 'http://localhost:8082', + cliArgs.v ? 'verbose' : '', + onFail, + supressors + ); +} + +main(); + +const onFail = (args, report) => { + try { + //console.log(report.clDataSorted[0]); + //console.log(report.legacyDataSorted[0]); + } catch (ex) {} + + //console.log(report.legacyDataSorted[0].counts); + + //console.log(JSON.stringify(args.data, null, 5)); + + //console.log(args.data.studyIds || args.data.studyViewFilter.studyIds); + + const url = 'http://localhost:8082' + args.url; + + const curl = ` + curl '${url}' + -H 'accept: application/json, text/plain, */*' + -H 'accept-language: en-US,en;q=0.9' + -H 'cache-control: no-cache' + -H 'content-type: application/json' + -H 'cookie: _ga_ET18FDC3P1=GS1.1.1727902893.87.0.1727902893.0.0.0; _gid=GA1.2.1570078648.1728481898; _ga_CKJ2CEEFD8=GS1.1.1728589307.172.1.1728589613.0.0.0; _ga_5260NDGD6Z=GS1.1.1728612388.318.1.1728612389.0.0.0; _gat_gtag_UA_17134933_2=1; _ga=GA1.1.1260093286.1710808634; _ga_334HHWHCPJ=GS1.1.1728647421.32.1.1728647514.0.0.0' + -H 'pragma: no-cache' + -H 'priority: u=1, i' + -H 'sec-ch-ua: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"' + -H 'sec-ch-ua-mobile: ?0' + -H 'sec-ch-ua-platform: "macOS"' + -H 'sec-fetch-dest: empty' + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36' + --data-raw '${JSON.stringify(args.data)}'; + `; + + cliArgs.c && + console.log( + curl + .trim() + .split('\n') + .join('') + ); + // + const studyIds = args.data.studyIds || args.data.studyViewFilter.studyIds; + cliArgs.u && + console.log( + `http://localhost:8082/study/summary?id=${studyIds.join( + ',' + )}#filterJson=${encodeURIComponent(JSON.stringify(args.data))}` + ); +}; + +function parseArgs() { + const args = process.argv.slice(2); + + const pairs = args.filter(s => /=/.test(s)); + + const single = args.filter(s => !/=/.test(s)); + + const obj = {}; + + single.forEach(a => { + obj[a.replace(/^-/, '')] = true; + }); + + pairs.forEach(p => { + const tuple = p.split('='); + obj[tuple[0].replace(/^-/, '')] = tuple[1]; + }); + + return obj; +} + +const supressorsPublic = [ + function(report) { + // @ts-ignore + return ( + report.clDataSorted[0].counts.length !== + report.legacyDataSorted[0].counts.length + ); + }, + function(report) { + return report.test.data.customDataFilters; + }, +]; + +const supressors = [ + function(report) { + return ( + report.clDataSorted[0].counts.find(m => m.value == 'not_mutated') + .count === + report.legacyDataSorted[0].counts.find( + m => m.value == 'not_profiled' + ).count + ); + }, + function(report) { + return ( + report.test.data.studyViewFilter.clinicalDataFilters[0].values + .length > 10 + ); + }, + function(report) { + return report.test.data.clinicalDataFilters[0].values.length > 10; + }, + function(report) { + return ( + report.test.data.customDataFilters || + report.test.data.studyViewFilter.customDataFilters + ); + }, + + function(report) { + return report.test.data.studyIds.includes('genie_private'); + }, +]; diff --git a/api-e2e/run.js b/api-e2e/run.js new file mode 100644 index 00000000000..b9f399b3f2d --- /dev/null +++ b/api-e2e/run.js @@ -0,0 +1,24 @@ +var json = require('./json/merged-tests.json'); +var axios = require('axios'); +var { validate, reportValidationResult, runSpecs } = require('./validation'); +const test = json[1].suites[0].tests[0]; + +const host = process.env.API_TEST_HOST || 'http://localhost:8082'; + +console.log(`RUNNING TESTS AGAINST: ${host}`); + +async function main() { + const start = Date.now(); + + const fileFilter = process.env.API_TEST_FILTER || ''; + + const files = fileFilter?.trim().length + ? json.filter(f => new RegExp(fileFilter).test(f.file)) + : json; + + await runSpecs(files, axios, host); + + //console.log(`Elapsed: ${Date.now() - start}`); +} + +main(); diff --git a/api-e2e/tsconfig.json b/api-e2e/tsconfig.json new file mode 100644 index 00000000000..1660a2a7230 --- /dev/null +++ b/api-e2e/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "outDir": "./" + }, + "types": [], + "files": ["./../src/shared/api/validation.ts"] +} \ No newline at end of file diff --git a/api-e2e/watch.js b/api-e2e/watch.js new file mode 100644 index 00000000000..5a53a131c97 --- /dev/null +++ b/api-e2e/watch.js @@ -0,0 +1,41 @@ +const fs = require('fs/promises'); +const watch = require('fs').watch; +const path = require('path'); + +// get rid of railing slash +const BACKEND_ROOT = (process.env.BACKEND_ROOT || '').replace(/\/$/, ''); + +const SPEC_ROOT = + '/Users/lismana/Documents/projects/cbioportal6/test/api-e2e/specs'; + +// watch(SPEC_ROOT, async function(event, filename) { +// if (event === 'change') { +// +// } +// }); + +async function mergeTests() { + const files = (await fs.readdir(SPEC_ROOT)).map(fileName => { + return path.join(SPEC_ROOT, fileName); + }); + + const jsons = files.map(path => { + return fs.readFile(path).then(data => { + try { + const json = JSON.parse(data); + return { file: path, suites: json }; + } catch (ex) { + console.log('invalid apiTest json spec'); + return []; + } + }); + }); + + Promise.all(jsons) + .then(d => { + fs.writeFile('./api-e2e/json/merged-tests.json', JSON.stringify(d)); + }) + .then(r => console.log('merged-tests.json written')); +} + +mergeTests(); diff --git a/package.json b/package.json index a538ce59d69..6028530b568 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ "scripts": { "start": "lerna run watch --parallel", "startSSL": "lerna run watchSSL --parallel", + "watchTests": "./scripts/api-test_env.sh && node ./api-e2e/watch.js", + "testpublic": "cd ./api-e2e && npx tsc && node ./run.js", + "apitests": "cd ./api-e2e && npx tsc && node ./run.js", "watch": "./scripts/env_vars.sh && eval \"$(./scripts/env_vars.sh)\" && cross-env NODE_ENV=development webpack-dev-server --compress", "watchSSL": "./scripts/env_vars.sh && eval \"$(./scripts/env_vars.sh)\" && cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=4096 webpack-dev-server --compress --https", "clean": "rimraf dist tsDist common-dist", @@ -31,7 +34,7 @@ "heroku-postbuild": "yarn run build && yarn add pushstate-server@3.0.1 -g", "updateAPI": "yarn run fetchAPI && yarn run buildAPI && yarn run updateOncoKbAPI && yarn run updateGenomeNexusAPI", "convertToSwagger2": "./scripts/convert_to_swagger2.sh && yarn run extendSwagger2Converter", - "fetchAPILocal": "export CBIOPORTAL_URL=http://localhost:8090 && curl -s -L -k ${CBIOPORTAL_URL}/api/v3/api-docs/public | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json && curl -s -L -k ${CBIOPORTAL_URL}/api/v3/api-docs/internal | json | grep -v host | grep -v basePath | grep -v termsOfService > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json", + "fetchAPILocal": "export CBIOPORTAL_URL=http://localhost:8082 && curl -s -L -k ${CBIOPORTAL_URL}/api/v3/api-docs/public > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json && curl -s -L -k ${CBIOPORTAL_URL}/api/v3/api-docs/internal | json > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json && yarn run convertToSwagger2", "fetchAPI": "./scripts/env_vars.sh && eval \"$(./scripts/env_vars.sh)\" && curl -s -L -k ${CBIOPORTAL_URL}/api/v3/api-docs/public > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json && curl -s -L -k ${CBIOPORTAL_URL}/api/v3/api-docs/internal | json > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json && yarn run convertToSwagger2", "extendSwagger2Converter": "node scripts/extend_converter.js packages/cbioportal-ts-api-client/src/generated CBioPortalAPI CBioPortalAPIInternal", "buildAPI": "node scripts/generate-api.js packages/cbioportal-ts-api-client/src/generated CBioPortalAPI CBioPortalAPIInternal", @@ -88,6 +91,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.4.4", "@babel/preset-env": "^7.4.4", "@babel/preset-react": "^7.0.0", + "@clickhouse/client": "^1.6.0", "@datadog/browser-logs": "^5.4.0", "@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-image": "^2.1.1", @@ -148,6 +152,7 @@ "addthis-snippet": "^1.0.1", "autobind-decorator": "^2.1.0", "autoprefixer": "^6.7.0", + "axios": "^1.7.7", "babel-loader": "8.0.5", "babel-plugin-transform-es2015-modules-umd": "^6.22.0", "babel-polyfill": "^6.22.0", @@ -172,6 +177,7 @@ "cross-env": "^3.1.4", "css-loader": "^2.1.1", "cssnano": "^3.10.0", + "csvtojson": "^2.0.10", "d3": "3.5.6", "d3-dsv": "1.0.8", "d3-scale": "^2.0.0", @@ -189,6 +195,7 @@ "fmin": "^0.0.2", "font-awesome": "^4.7.0", "fork-ts-checker-webpack-plugin": "^6.3.3", + "format-curl": "^2.2.1", "genome-nexus-ts-api-client": "^1.1.32", "git-revision-webpack-plugin": "^5.0.0", "history": "4.10.1", @@ -226,6 +233,7 @@ "mobx-react-lite": "3.0.1", "mobx-react-router": "4.1.0", "mobx-utils": "6.0.1", + "najax": "^1.0.7", "numeral": "^2.0.6", "object-sizeof": "^1.2.0", "oncokb-frontend-commons": "^0.0.25", diff --git a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json index a8616dca66f..15f51e83d39 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json @@ -558,6 +558,41 @@ "operationId": "getClinicalEventTypeCountsUsingPOST" } }, + "/api/clinical-events-meta/fetch": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "clinicalEventAttributeRequest", + "schema": { + "$ref": "#/definitions/ClinicalEventAttributeRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/ClinicalEvent" + }, + "type": "array" + } + } + }, + "tags": [ + "Clinical Events" + ], + "description": "Fetch clinical events meta", + "operationId": "fetchClinicalEventsMetaUsingPOST" + } + }, "/api/cna-genes/fetch": { "post": { "consumes": [ @@ -593,6 +628,93 @@ "operationId": "fetchCNAGenesUsingPOST" } }, + "/api/column-store/treatments/patient-counts/fetch": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "default": "Agent", + "enum": [ + "Agent", + "AgentClass", + "AgentTarget" + ], + "in": "query", + "name": "tier", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "studyViewFilter", + "schema": { + "$ref": "#/definitions/StudyViewFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/PatientTreatmentReport" + } + } + }, + "tags": [ + "study-view-column-store-controller" + ], + "description": "Get all patient level treatments", + "operationId": "fetchPatientTreatmentCountsUsing" + } + }, + "/api/column-store/treatments/sample-counts/fetch": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "default": "Agent", + "enum": [ + "Agent", + "AgentClass", + "AgentTarget" + ], + "in": "query", + "name": "tier", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "studyViewFilter", + "schema": { + "$ref": "#/definitions/StudyViewFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SampleTreatmentReport" + } + } + }, + "tags": [ + "study-view-column-store-controller" + ], + "operationId": "fetchSampleTreatmentCountsUsing" + } + }, "/api/cosmic-counts/fetch": { "post": { "consumes": [ @@ -3219,6 +3341,41 @@ "operationId": "getSignificantlyMutatedGenesUsingGET" } }, + "/api/survival-data/fetch": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "survivalRequest", + "schema": { + "$ref": "#/definitions/SurvivalRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/ClinicalData" + }, + "type": "array" + } + } + }, + "tags": [ + "Survival" + ], + "description": "Fetch survival data", + "operationId": "fetchSurvivalDataUsingPOST" + } + }, "/api/timestamps": { "get": { "produces": [ @@ -3958,6 +4115,27 @@ ], "type": "object" }, + "ClinicalEventAttributeRequest": { + "description": "clinical events Request", + "properties": { + "clinicalEventRequests": { + "items": { + "$ref": "#/definitions/ClinicalEventRequest" + }, + "type": "array", + "uniqueItems": true + }, + "patientIdentifiers": { + "items": { + "$ref": "#/definitions/PatientIdentifier" + }, + "maxItems": 10000000, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, "ClinicalEventData": { "properties": { "key": { @@ -3973,6 +4151,47 @@ ], "type": "object" }, + "ClinicalEventRequest": { + "properties": { + "attributes": { + "items": { + "$ref": "#/definitions/ClinicalEventData" + }, + "type": "array" + }, + "eventType": { + "type": "string" + } + }, + "required": [ + "eventType" + ], + "type": "object" + }, + "ClinicalEventRequestIdentifier": { + "properties": { + "clinicalEventRequests": { + "items": { + "$ref": "#/definitions/ClinicalEventRequest" + }, + "maxItems": 10000000, + "minItems": 0, + "type": "array", + "uniqueItems": true + }, + "position": { + "enum": [ + "FIRST", + "LAST" + ], + "type": "string" + } + }, + "required": [ + "position" + ], + "type": "object" + }, "ClinicalEventSample": { "properties": { "patientId": { @@ -5464,6 +5683,29 @@ }, "type": "object" }, + "PatientIdentifier": { + "properties": { + "patientId": { + "type": "string" + }, + "studyId": { + "type": "string" + } + }, + "type": "object" + }, + "PatientTreatment": { + "properties": { + "count": { + "format": "int32", + "type": "integer" + }, + "treatment": { + "type": "string" + } + }, + "type": "object" + }, "PatientTreatmentFilter": { "properties": { "treatment": { @@ -5472,6 +5714,25 @@ }, "type": "object" }, + "PatientTreatmentReport": { + "properties": { + "patientTreatments": { + "items": { + "$ref": "#/definitions/PatientTreatment" + }, + "type": "array" + }, + "totalPatients": { + "format": "int32", + "type": "integer" + }, + "totalSamples": { + "format": "int32", + "type": "integer" + } + }, + "type": "object" + }, "PatientTreatmentRow": { "properties": { "count": { @@ -5690,6 +5951,21 @@ }, "type": "object" }, + "SampleTreatmentReport": { + "properties": { + "totalSamples": { + "format": "int32", + "type": "integer" + }, + "treatments": { + "items": { + "$ref": "#/definitions/SampleTreatmentRow" + }, + "type": "array" + } + }, + "type": "object" + }, "SampleTreatmentRow": { "properties": { "count": { @@ -6126,6 +6402,35 @@ }, "type": "object" }, + "SurvivalRequest": { + "description": "Survival Data Request", + "properties": { + "attributeIdPrefix": { + "type": "string" + }, + "censoredEventRequestIdentifier": { + "$ref": "#/definitions/ClinicalEventRequestIdentifier" + }, + "endEventRequestIdentifier": { + "$ref": "#/definitions/ClinicalEventRequestIdentifier" + }, + "patientIdentifiers": { + "items": { + "$ref": "#/definitions/PatientIdentifier" + }, + "maxItems": 10000000, + "minItems": 1, + "type": "array" + }, + "startEventRequestIdentifier": { + "$ref": "#/definitions/ClinicalEventRequestIdentifier" + } + }, + "required": [ + "attributeIdPrefix" + ], + "type": "object" + }, "VariantCount": { "properties": { "entrezGeneId": { diff --git a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts index 02610206a86..ca9bc28d658 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts @@ -238,12 +238,30 @@ export type ClinicalEvent = { 'uniqueSampleKey': string +}; +export type ClinicalEventAttributeRequest = { + 'clinicalEventRequests': Array < ClinicalEventRequest > + + 'patientIdentifiers': Array < PatientIdentifier > + }; export type ClinicalEventData = { 'key': string 'value': string +}; +export type ClinicalEventRequest = { + 'attributes': Array < ClinicalEventData > + + 'eventType': string + +}; +export type ClinicalEventRequestIdentifier = { + 'clinicalEventRequests': Array < ClinicalEventRequest > + + 'position': "FIRST" | "LAST" + }; export type ClinicalEventSample = { 'patientId': string @@ -896,10 +914,30 @@ export type OredPatientTreatmentFilters = { export type OredSampleTreatmentFilters = { 'filters': Array < SampleTreatmentFilter > +}; +export type PatientIdentifier = { + 'patientId': string + + 'studyId': string + +}; +export type PatientTreatment = { + 'count': number + + 'treatment': string + }; export type PatientTreatmentFilter = { 'treatment': string +}; +export type PatientTreatmentReport = { + 'patientTreatments': Array < PatientTreatment > + + 'totalPatients': number + + 'totalSamples': number + }; export type PatientTreatmentRow = { 'count': number @@ -998,6 +1036,12 @@ export type SampleTreatmentFilter = { 'treatment': string +}; +export type SampleTreatmentReport = { + 'totalSamples': number + + 'treatments': Array < SampleTreatmentRow > + }; export type SampleTreatmentRow = { 'count': number @@ -1203,6 +1247,18 @@ export type StudyViewStructuralVariantFilter = { 'structVarQueries': Array < Array < StructuralVariantFilterQuery > > +}; +export type SurvivalRequest = { + 'attributeIdPrefix': string + + 'censoredEventRequestIdentifier': ClinicalEventRequestIdentifier + + 'endEventRequestIdentifier': ClinicalEventRequestIdentifier + + 'patientIdentifiers': Array < PatientIdentifier > + + 'startEventRequestIdentifier': ClinicalEventRequestIdentifier + }; export type VariantCount = { 'entrezGeneId': number @@ -2339,6 +2395,78 @@ export default class CBioPortalAPIInternal { return response.body; }); }; + fetchClinicalEventsMetaUsingPOSTURL(parameters: { + 'clinicalEventAttributeRequest' ? : ClinicalEventAttributeRequest, + $queryParameters ? : any + }): string { + let queryParameters: any = {}; + let path = '/api/clinical-events-meta/fetch'; + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + let keys = Object.keys(queryParameters); + return this.domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : ''); + }; + + /** + * Fetch clinical events meta + * @method + * @name CBioPortalAPIInternal#fetchClinicalEventsMetaUsingPOST + * @param {} clinicalEventAttributeRequest - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + */ + fetchClinicalEventsMetaUsingPOSTWithHttpInfo(parameters: { + 'clinicalEventAttributeRequest' ? : ClinicalEventAttributeRequest, + $queryParameters ? : any, + $domain ? : string + }): Promise < request.Response > { + const domain = parameters.$domain ? parameters.$domain : this.domain; + const errorHandlers = this.errorHandlers; + const request = this.request; + let path = '/api/clinical-events-meta/fetch'; + let body: any; + let queryParameters: any = {}; + let headers: any = {}; + let form: any = {}; + return new Promise(function(resolve, reject) { + headers['Accept'] = 'application/json'; + headers['Content-Type'] = 'application/json'; + + if (parameters['clinicalEventAttributeRequest'] !== undefined) { + body = parameters['clinicalEventAttributeRequest']; + } + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + + request('POST', domain + path, body, headers, queryParameters, form, reject, resolve, errorHandlers); + + }); + }; + + /** + * Fetch clinical events meta + * @method + * @name CBioPortalAPIInternal#fetchClinicalEventsMetaUsingPOST + * @param {} clinicalEventAttributeRequest - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + */ + fetchClinicalEventsMetaUsingPOST(parameters: { + 'clinicalEventAttributeRequest' ? : ClinicalEventAttributeRequest, + $queryParameters ? : any, + $domain ? : string + }): Promise < Array < ClinicalEvent > + > { + return this.fetchClinicalEventsMetaUsingPOSTWithHttpInfo(parameters).then(function(response: request.Response) { + return response.body; + }); + }; fetchCNAGenesUsingPOSTURL(parameters: { 'studyViewFilter' ? : StudyViewFilter, $queryParameters ? : any @@ -2411,6 +2539,172 @@ export default class CBioPortalAPIInternal { return response.body; }); }; + fetchPatientTreatmentCountsUsingURL(parameters: { + 'tier' ? : "Agent" | "AgentClass" | "AgentTarget", + 'studyViewFilter' ? : StudyViewFilter, + $queryParameters ? : any + }): string { + let queryParameters: any = {}; + let path = '/api/column-store/treatments/patient-counts/fetch'; + if (parameters['tier'] !== undefined) { + queryParameters['tier'] = parameters['tier']; + } + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + let keys = Object.keys(queryParameters); + return this.domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : ''); + }; + + /** + * Get all patient level treatments + * @method + * @name CBioPortalAPIInternal#fetchPatientTreatmentCountsUsing + * @param {string} tier - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + * @param {} studyViewFilter - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + */ + fetchPatientTreatmentCountsUsingWithHttpInfo(parameters: { + 'tier' ? : "Agent" | "AgentClass" | "AgentTarget", + 'studyViewFilter' ? : StudyViewFilter, + $queryParameters ? : any, + $domain ? : string + }): Promise < request.Response > { + const domain = parameters.$domain ? parameters.$domain : this.domain; + const errorHandlers = this.errorHandlers; + const request = this.request; + let path = '/api/column-store/treatments/patient-counts/fetch'; + let body: any; + let queryParameters: any = {}; + let headers: any = {}; + let form: any = {}; + return new Promise(function(resolve, reject) { + headers['Accept'] = 'application/json'; + headers['Content-Type'] = 'application/json'; + + if (parameters['tier'] !== undefined) { + queryParameters['tier'] = parameters['tier']; + } + + if (parameters['studyViewFilter'] !== undefined) { + body = parameters['studyViewFilter']; + } + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + + request('POST', domain + path, body, headers, queryParameters, form, reject, resolve, errorHandlers); + + }); + }; + + /** + * Get all patient level treatments + * @method + * @name CBioPortalAPIInternal#fetchPatientTreatmentCountsUsing + * @param {string} tier - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + * @param {} studyViewFilter - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + */ + fetchPatientTreatmentCountsUsing(parameters: { + 'tier' ? : "Agent" | "AgentClass" | "AgentTarget", + 'studyViewFilter' ? : StudyViewFilter, + $queryParameters ? : any, + $domain ? : string + }): Promise < PatientTreatmentReport > { + return this.fetchPatientTreatmentCountsUsingWithHttpInfo(parameters).then(function(response: request.Response) { + return response.body; + }); + }; + fetchSampleTreatmentCountsUsingURL(parameters: { + 'tier' ? : "Agent" | "AgentClass" | "AgentTarget", + 'studyViewFilter' ? : StudyViewFilter, + $queryParameters ? : any + }): string { + let queryParameters: any = {}; + let path = '/api/column-store/treatments/sample-counts/fetch'; + if (parameters['tier'] !== undefined) { + queryParameters['tier'] = parameters['tier']; + } + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + let keys = Object.keys(queryParameters); + return this.domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : ''); + }; + + /** + * + * @method + * @name CBioPortalAPIInternal#fetchSampleTreatmentCountsUsing + * @param {string} tier - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + * @param {} studyViewFilter - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + */ + fetchSampleTreatmentCountsUsingWithHttpInfo(parameters: { + 'tier' ? : "Agent" | "AgentClass" | "AgentTarget", + 'studyViewFilter' ? : StudyViewFilter, + $queryParameters ? : any, + $domain ? : string + }): Promise < request.Response > { + const domain = parameters.$domain ? parameters.$domain : this.domain; + const errorHandlers = this.errorHandlers; + const request = this.request; + let path = '/api/column-store/treatments/sample-counts/fetch'; + let body: any; + let queryParameters: any = {}; + let headers: any = {}; + let form: any = {}; + return new Promise(function(resolve, reject) { + headers['Accept'] = 'application/json'; + headers['Content-Type'] = 'application/json'; + + if (parameters['tier'] !== undefined) { + queryParameters['tier'] = parameters['tier']; + } + + if (parameters['studyViewFilter'] !== undefined) { + body = parameters['studyViewFilter']; + } + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + + request('POST', domain + path, body, headers, queryParameters, form, reject, resolve, errorHandlers); + + }); + }; + + /** + * + * @method + * @name CBioPortalAPIInternal#fetchSampleTreatmentCountsUsing + * @param {string} tier - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + * @param {} studyViewFilter - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + */ + fetchSampleTreatmentCountsUsing(parameters: { + 'tier' ? : "Agent" | "AgentClass" | "AgentTarget", + 'studyViewFilter' ? : StudyViewFilter, + $queryParameters ? : any, + $domain ? : string + }): Promise < SampleTreatmentReport > { + return this.fetchSampleTreatmentCountsUsingWithHttpInfo(parameters).then(function(response: request.Response) { + return response.body; + }); + }; fetchCosmicCountsUsingPOSTURL(parameters: { 'keywords': Array < string > , $queryParameters ? : any @@ -7314,6 +7608,78 @@ export default class CBioPortalAPIInternal { return response.body; }); }; + fetchSurvivalDataUsingPOSTURL(parameters: { + 'survivalRequest' ? : SurvivalRequest, + $queryParameters ? : any + }): string { + let queryParameters: any = {}; + let path = '/api/survival-data/fetch'; + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + let keys = Object.keys(queryParameters); + return this.domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : ''); + }; + + /** + * Fetch survival data + * @method + * @name CBioPortalAPIInternal#fetchSurvivalDataUsingPOST + * @param {} survivalRequest - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + */ + fetchSurvivalDataUsingPOSTWithHttpInfo(parameters: { + 'survivalRequest' ? : SurvivalRequest, + $queryParameters ? : any, + $domain ? : string + }): Promise < request.Response > { + const domain = parameters.$domain ? parameters.$domain : this.domain; + const errorHandlers = this.errorHandlers; + const request = this.request; + let path = '/api/survival-data/fetch'; + let body: any; + let queryParameters: any = {}; + let headers: any = {}; + let form: any = {}; + return new Promise(function(resolve, reject) { + headers['Accept'] = 'application/json'; + headers['Content-Type'] = 'application/json'; + + if (parameters['survivalRequest'] !== undefined) { + body = parameters['survivalRequest']; + } + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + + request('POST', domain + path, body, headers, queryParameters, form, reject, resolve, errorHandlers); + + }); + }; + + /** + * Fetch survival data + * @method + * @name CBioPortalAPIInternal#fetchSurvivalDataUsingPOST + * @param {} survivalRequest - A web service for supplying JSON formatted data to cBioPortal clients. Please note that this API is currently in beta and subject to change. + */ + fetchSurvivalDataUsingPOST(parameters: { + 'survivalRequest' ? : SurvivalRequest, + $queryParameters ? : any, + $domain ? : string + }): Promise < Array < ClinicalData > + > { + return this.fetchSurvivalDataUsingPOSTWithHttpInfo(parameters).then(function(response: request.Response) { + return response.body; + }); + }; getAllTimestampsUsingGETURL(parameters: { $queryParameters ? : any }): string { diff --git a/packages/cbioportal-ts-api-client/src/index.tsx b/packages/cbioportal-ts-api-client/src/index.tsx index 5965c0a825d..7472a08a5f1 100644 --- a/packages/cbioportal-ts-api-client/src/index.tsx +++ b/packages/cbioportal-ts-api-client/src/index.tsx @@ -89,6 +89,9 @@ export { SampleTreatmentFilter, PatientTreatmentFilter, PatientTreatmentRow, + PatientTreatmentReport, + SampleTreatmentReport, + PatientTreatment, MutationDataFilter, GenericAssayDataFilter, AlterationFilter, diff --git a/scripts/api-test_env.sh b/scripts/api-test_env.sh new file mode 100755 index 00000000000..46dcb1511c9 --- /dev/null +++ b/scripts/api-test_env.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# eval output of this file to get appropriate env variables e.g. eval "$(./env_vars.sh)" +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +RED='\033[0;31m' +NC='\033[0m' + + +if [[ "$BACKEND_ROOT" ]]; then + exit 0 +else + echo -e "${RED}No desired BACKEND_ROOT variable set${NC}" + echo -e "${RED}set with e.g. export BACKEND_ROOT=/path/to/my/backend/repo/${NC}" + exit 1 +fi diff --git a/src/appBootstrapper.tsx b/src/appBootstrapper.tsx index 901a49bff29..8f2f2981a0a 100755 --- a/src/appBootstrapper.tsx +++ b/src/appBootstrapper.tsx @@ -144,7 +144,9 @@ browserWindow.postLoadForMskCIS = () => {}; (browserWindow as any).$3Dmol = { notrack: true }; // make sure lodash doesn't overwrite (or set) global underscore -_.noConflict(); +//_.noConflict(); + +getBrowserWindow()._ = _; const routingStore = new ExtendedRoutingStore(); diff --git a/src/config/config.ts b/src/config/config.ts index 9e096df6b07..b8baa6f2b20 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -20,7 +20,9 @@ import internalGenomeNexusClient from '../shared/api/genomeNexusInternalClientIn import oncoKBClient from '../shared/api/oncokbClientInstance'; import genome2StructureClient from '../shared/api/g2sClientInstance'; import client from '../shared/api/cbioportalClientInstance'; -import internalClient from '../shared/api/cbioportalInternalClientInstance'; +import internalClient, { + internalClientColumnStore, +} from '../shared/api/cbioportalInternalClientInstance'; import $ from 'jquery'; import { AppStore } from '../AppStore'; import { CBioPortalAPI, CBioPortalAPIInternal } from 'cbioportal-ts-api-client'; @@ -194,6 +196,7 @@ export function initializeAPIClients() { // we need to set the domain of our api clients (client as any).domain = getCbioPortalApiUrl(); (internalClient as any).domain = getCbioPortalApiUrl(); + (internalClientColumnStore as any).domain = getCbioPortalApiUrl(); (genomeNexusClient as any).domain = getGenomeNexusApiUrl(); (internalGenomeNexusClient as any).domain = getGenomeNexusApiUrl(); (oncoKBClient as any).domain = getOncoKbApiUrl(); diff --git a/src/pages/studyView/StudyViewPage.tsx b/src/pages/studyView/StudyViewPage.tsx index 508fe36e461..1a0ef43111c 100644 --- a/src/pages/studyView/StudyViewPage.tsx +++ b/src/pages/studyView/StudyViewPage.tsx @@ -27,7 +27,9 @@ import IFrameLoader from '../../shared/components/iframeLoader/IFrameLoader'; import { StudySummaryTab } from 'pages/studyView/tabs/SummaryTab'; import StudyPageHeader from './studyPageHeader/StudyPageHeader'; import CNSegments from './tabs/CNSegments'; - +import internalClient, { + internalClientColumnStore, +} from 'shared/api/cbioportalInternalClientInstance'; import AddChartButton from './addChartButton/AddChartButton'; import { sleep } from '../../shared/lib/TimeUtils'; import { Else, If, Then } from 'react-if'; @@ -79,6 +81,7 @@ import { } from 'shared/lib/customTabs/customTabHelpers'; import { VirtualStudyModal } from 'pages/studyView/virtualStudy/VirtualStudyModal'; import PlotsTab from 'shared/components/plots/PlotsTab'; +import { RFC80Test } from 'pages/studyView/rfc80Tester'; export interface IStudyViewPageProps { routing: any; @@ -145,9 +148,19 @@ export default class StudyViewPage extends React.Component< this.store = new StudyViewPageStore( this.props.appStore, ServerConfigHelpers.sessionServiceIsEnabled(), - this.urlWrapper + this.urlWrapper, + this.props.routing.query.legacy === '1' + ? internalClient + : internalClientColumnStore ); + // this.store_column_store = new StudyViewPageStore( + // this.props.appStore, + // ServerConfigHelpers.sessionServiceIsEnabled(), + // this.urlWrapper, + // internalClientColumnStore + // ); + // Expose store to window for use in custom tabs. setWindowVariable('studyViewPageStore', this.store); @@ -171,7 +184,7 @@ export default class StudyViewPage extends React.Component< const query = props.routing.query; const hash = props.routing.location.hash; // clear hash if any - props.routing.location.hash = ''; + //props.routing.location.hash = ''; const newStudyViewFilter: StudyViewURLQuery = _.pick(query, [ 'id', 'studyId', @@ -674,21 +687,34 @@ export default class StudyViewPage extends React.Component< store={this.store} > - - - + + {/**/} + {/* */} + {/**/} + + {/**/} + {/* */} + {/**/} - - - + {/**/} + {/* */} + {/**/} - - { - StudyViewPageTabDescriptions.PLOTS - }{' '} - - Beta! - - - } - > - - + {/**/} + {/* {*/} + {/* StudyViewPageTabDescriptions.PLOTS*/} + {/* }{' '}*/} + {/* */} + {/* Beta!*/} + {/* */} + {/* */} + {/* }*/} + {/*>*/} + {/* */} + {/**/} {this.resourceTabs.component} {this.customTabs} @@ -1238,6 +1264,7 @@ export default class StudyViewPage extends React.Component< isLoggedIn={this.props.appStore.isLoggedIn} /> )} + ); } diff --git a/src/pages/studyView/StudyViewPageStore.ts b/src/pages/studyView/StudyViewPageStore.ts index 98d9da7d788..10d55739a1d 100644 --- a/src/pages/studyView/StudyViewPageStore.ts +++ b/src/pages/studyView/StudyViewPageStore.ts @@ -1,5 +1,4 @@ import _ from 'lodash'; -import internalClient from 'shared/api/cbioportalInternalClientInstance'; import defaultClient from 'shared/api/cbioportalClientInstance'; import client from 'shared/api/cbioportalClientInstance'; import oncoKBClient from 'shared/api/oncokbClientInstance'; @@ -19,6 +18,7 @@ import { AndedSampleTreatmentFilters, BinsGeneratorConfig, CancerStudy, + CBioPortalAPIInternal, ClinicalAttribute, ClinicalAttributeCount, ClinicalAttributeCountFilter, @@ -64,7 +64,6 @@ import { OredPatientTreatmentFilters, OredSampleTreatmentFilters, Patient, - PatientTreatmentRow, ResourceData, Sample, SampleFilter, @@ -76,6 +75,8 @@ import { StructuralVariantFilterQuery, StudyViewFilter, StudyViewStructuralVariantFilter, + GenericAssayDataCountFilter, + GenericAssayDataCountItem, } from 'cbioportal-ts-api-client'; import { evaluatePutativeDriverInfo, @@ -216,6 +217,7 @@ import { } from '../../shared/api/urls'; import { DataType as DownloadDataType, + getBrowserWindow, MobxPromise, onMobxPromise, pluralize, @@ -374,6 +376,8 @@ import { PlotsSelectionParam, } from 'pages/resultsView/ResultsViewURLWrapper'; import { SortDirection } from 'shared/components/lazyMobXTable/LazyMobXTable'; +import CbioportalInternalClientInstance from 'shared/api/cbioportalInternalClientInstance'; +import CbioportalClientInstance from 'shared/api/cbioportalClientInstance'; export const STUDY_VIEW_FILTER_AUTOSUBMIT = 'study_view_filter_autosubmit'; @@ -602,7 +606,8 @@ export class StudyViewPageStore constructor( public appStore: AppStore, private sessionServiceIsEnabled: boolean, - private urlWrapper: StudyViewURLWrapper + private urlWrapper: StudyViewURLWrapper, + public internalClient: CBioPortalAPIInternal ) { makeObservable(this); @@ -2067,7 +2072,11 @@ export class StudyViewPageStore @observable private initialFiltersQuery: Partial = {}; - @observable studyIds: string[] = []; + @observable _selectedStudyIds: string[] | undefined; + @observable _studyIds: string[] = []; + @computed get studyIds(): string[] { + return this._selectedStudyIds || this._studyIds; + } private _clinicalDataFilterSet = observable.map< AttributeId, @@ -2865,6 +2874,8 @@ export class StudyViewPageStore this.clearSampleTreatmentFilters(); this.clearSampleTreatmentGroupFilters(); this.clearSampleTreatmentTargetFilters(); + // For cancer studies chart reset + this._selectedStudyIds = undefined; if (this.hesitateUpdate) { this.filters = this.filtersProxy; } @@ -3078,16 +3089,8 @@ export class StudyViewPageStore if ( chartUniqueKey === SpecialChartsUniqueKeyEnum.CANCER_STUDIES ) { + this._selectedStudyIds = values; // this is for pre-defined custom charts - let filteredSampleIdentifiers = getFilteredSampleIdentifiers( - this.samples.result.filter(sample => - values.includes(sample.studyId) - ) - ); - this._chartSampleIdentifiersFilterSet.set( - chartUniqueKey, - filteredSampleIdentifiers - ); this.preDefinedCustomChartFilterSet.set( chartUniqueKey, clinicalDataFilter @@ -3102,10 +3105,8 @@ export class StudyViewPageStore if ( chartUniqueKey === SpecialChartsUniqueKeyEnum.CANCER_STUDIES ) { + this._selectedStudyIds = undefined; // this is for pre-defined custom charts - this._chartSampleIdentifiersFilterSet.delete( - chartUniqueKey - ); this.preDefinedCustomChartFilterSet.delete(chartUniqueKey); } else { this._customDataFilterSet.delete(chartUniqueKey); @@ -4518,7 +4519,7 @@ export class StudyViewPageStore readonly unfilteredClinicalDataCount = remoteData({ invoke: () => { if (!_.isEmpty(this.unfilteredAttrsForNonNumerical)) { - return internalClient.fetchClinicalDataCountsUsingPOST({ + return this.internalClient.fetchClinicalDataCountsUsingPOST({ clinicalDataCountFilter: { attributes: this.unfilteredAttrsForNonNumerical, studyViewFilter: this.filters, @@ -4545,7 +4546,7 @@ export class StudyViewPageStore invoke: () => { //only invoke if there are filtered samples if (this.hasFilteredSamples) { - return internalClient.fetchCustomDataCountsUsingPOST({ + return this.internalClient.fetchCustomDataCountsUsingPOST({ clinicalDataCountFilter: { attributes: this.unfilteredCustomAttrsForNonNumerical, studyViewFilter: this.filters, @@ -4572,7 +4573,7 @@ export class StudyViewPageStore >({ invoke: () => { if (!_.isEmpty(this.newlyAddedUnfilteredAttrsForNonNumerical)) { - return internalClient.fetchClinicalDataCountsUsingPOST({ + return this.internalClient.fetchClinicalDataCountsUsingPOST({ clinicalDataCountFilter: { attributes: this .newlyAddedUnfilteredAttrsForNonNumerical, @@ -4601,7 +4602,7 @@ export class StudyViewPageStore this.hasSampleIdentifiersInFilter && this.newlyAddedUnfilteredAttrsForNumerical.length > 0 ) { - const clinicalDataBinCountData = await internalClient.fetchClinicalDataBinCountsUsingPOST( + const clinicalDataBinCountData = await this.internalClient.fetchClinicalDataBinCountsUsingPOST( { dataBinMethod: 'STATIC', clinicalDataBinCountFilter: { @@ -4642,7 +4643,7 @@ export class StudyViewPageStore return element !== undefined; } ); - const clinicalDataBinCountData = await internalClient.fetchClinicalDataBinCountsUsingPOST( + const clinicalDataBinCountData = await this.internalClient.fetchClinicalDataBinCountsUsingPOST( { dataBinMethod: 'STATIC', clinicalDataBinCountFilter: { @@ -4768,7 +4769,7 @@ export class StudyViewPageStore this._clinicalDataFilterSet.has(uniqueKey) || this.isInitialFilterState ) { - result = await internalClient.fetchClinicalDataCountsUsingPOST( + result = await this.internalClient.fetchClinicalDataCountsUsingPOST( { clinicalDataCountFilter: { attributes: [ @@ -4850,7 +4851,7 @@ export class StudyViewPageStore if (!this.hasFilteredSamples) { return []; } - result = await internalClient.fetchCustomDataCountsUsingPOST( + result = await this.internalClient.fetchCustomDataCountsUsingPOST( { clinicalDataCountFilter: { attributes: [ @@ -4898,7 +4899,7 @@ export class StudyViewPageStore return element !== undefined; } ); - const result2 = await internalClient.fetchCustomDataBinCountsUsingPOST( + const result2 = await this.internalClient.fetchCustomDataBinCountsUsingPOST( { dataBinMethod: 'STATIC', clinicalDataBinCountFilter: { @@ -4930,7 +4931,7 @@ export class StudyViewPageStore const attribute: ClinicalDataBinFilter = getDefaultClinicalDataBinFilter( chartMeta.clinicalAttribute! ); - const result = await internalClient.fetchCustomDataBinCountsUsingPOST( + const result = await this.internalClient.fetchCustomDataBinCountsUsingPOST( { dataBinMethod: 'STATIC', clinicalDataBinCountFilter: { @@ -4977,7 +4978,7 @@ export class StudyViewPageStore const attribute: ClinicalDataBinFilter = this._customDataBinFilterSet.get( uniqueKey )!; - const result = await internalClient.fetchCustomDataBinCountsUsingPOST( + const result = await this.internalClient.fetchCustomDataBinCountsUsingPOST( { dataBinMethod: 'STATIC', clinicalDataBinCountFilter: { @@ -5030,26 +5031,46 @@ export class StudyViewPageStore chartMeta.uniqueKey ); if (chartInfo) { - const result = await invokeGenericAssayDataCount( - chartInfo, - this.filters + let result: GenericAssayDataCountItem[] = []; + + result = await this.internalClient.fetchGenericAssayDataCountsUsingPOST( + { + genericAssayDataCountFilter: { + genericAssayDataFilters: [ + { + stableId: + chartInfo.genericAssayEntityId, + profileType: chartInfo.profileType, + } as GenericAssayDataFilter, + ], + studyViewFilter: this.filters, + } as GenericAssayDataCountFilter, + } ); if (_.isEmpty(result)) { return res; } - if (!this.chartToUsedColors.has(result!.stableId)) { - this.chartToUsedColors.set( - result!.stableId, - new Set() - ); + let data = result.find( + d => d.stableId === chartInfo.genericAssayEntityId + ); + let counts: ClinicalDataCount[] = []; + let stableId: string = ''; + if (data !== undefined) { + counts = data.counts.map(c => { + return { + count: c.count, + value: c.value, + } as ClinicalDataCount; + }); + stableId = data.stableId; + if (!this.chartToUsedColors.has(stableId)) { + this.chartToUsedColors.set(stableId, new Set()); + } } - return this.addColorToCategories( - result!.counts, - result!.stableId - ); + return this.addColorToCategories(counts, stableId); } return res; }, @@ -5209,7 +5230,7 @@ export class StudyViewPageStore if (!this.hasSampleIdentifiersInFilter) { return []; } - result = await internalClient.fetchClinicalDataBinCountsUsingPOST( + result = await this.internalClient.fetchClinicalDataBinCountsUsingPOST( { dataBinMethod, clinicalDataBinCountFilter: { @@ -5265,7 +5286,7 @@ export class StudyViewPageStore )!; //only invoke if there are filtered samples if (chartInfo && this.hasFilteredSamples) { - const genomicDataBins = await internalClient.fetchGenomicDataBinCountsUsingPOST( + const genomicDataBins = await this.internalClient.fetchGenomicDataBinCountsUsingPOST( { dataBinMethod: DataBinMethodConstants.STATIC, genomicDataBinCountFilter: { @@ -5315,7 +5336,7 @@ export class StudyViewPageStore chartMeta.uniqueKey )!; if (chartInfo) { - const gaDataBins = await internalClient.fetchGenericAssayDataBinCountsUsingPOST( + const gaDataBins = await this.internalClient.fetchGenericAssayDataBinCountsUsingPOST( { dataBinMethod: DataBinMethodConstants.STATIC, genericAssayDataBinCountFilter: { @@ -5479,6 +5500,113 @@ export class StudyViewPageStore default: [], }); + // contains queried physical studies + readonly filteredPhysicalStudiesFromOriginalQuery = remoteData({ + await: () => [this.everyStudyIdToStudy], + invoke: async () => { + const everyStudyIdToStudy = this.everyStudyIdToStudy.result!; + return _.reduce( + this._studyIds, + (acc: CancerStudy[], next) => { + if ( + everyStudyIdToStudy[next] && + isQueriedStudyAuthorized(everyStudyIdToStudy[next]) + ) { + acc.push(everyStudyIdToStudy[next]); + } + return acc; + }, + [] + ); + }, + default: [], + }); + + // contains queried vaild virtual studies + readonly filteredVirtualStudiesFromOriginalQuery = remoteData({ + await: () => [this.filteredPhysicalStudiesFromOriginalQuery], + invoke: async () => { + if ( + this.filteredPhysicalStudiesFromOriginalQuery.result.length === + this._studyIds.length + ) { + return []; + } + let filteredVirtualStudies: VirtualStudy[] = []; + let validFilteredPhysicalStudyIds = this.filteredPhysicalStudiesFromOriginalQuery.result.map( + study => study.studyId + ); + let virtualStudyIds = _.filter( + this._studyIds, + id => !_.includes(validFilteredPhysicalStudyIds, id) + ); + + await Promise.all( + virtualStudyIds.map(id => + sessionServiceClient + .getVirtualStudy(id) + .then(res => { + filteredVirtualStudies.push(res); + }) + .catch(() => { + /*do nothing*/ + }) + ) + ); + return filteredVirtualStudies; + }, + default: [], + }); + + // includes all physical studies from queried virtual studies + readonly queriedPhysicalStudiesFromOriginalQuery = remoteData({ + await: () => [ + this.filteredPhysicalStudiesFromOriginalQuery, + this.filteredVirtualStudiesFromOriginalQuery, + ], + invoke: async () => { + let everyStudyIdToStudy = this.everyStudyIdToStudy.result!; + + let studies = _.reduce( + this.filteredPhysicalStudiesFromOriginalQuery.result, + (acc, next) => { + acc[next.studyId] = everyStudyIdToStudy[next.studyId]; + return acc; + }, + {} as { [id: string]: CancerStudy } + ); + + this.filteredVirtualStudiesFromOriginalQuery.result.forEach( + virtualStudy => { + virtualStudy.data.studies.forEach(study => { + if ( + !studies[study.id] && + everyStudyIdToStudy[study.id] + ) { + studies[study.id] = everyStudyIdToStudy[study.id]; + } + }); + } + ); + return _.values(studies); + }, + default: [], + }); + + // includes all physical studies from queried virtual studies + readonly queriedPhysicalStudyIdsFromOriginalQuery = remoteData({ + await: () => [this.queriedPhysicalStudiesFromOriginalQuery], + invoke: () => { + return Promise.resolve( + _.map( + this.queriedPhysicalStudiesFromOriginalQuery.result, + study => study.studyId + ) + ); + }, + default: [], + }); + readonly studyIdToStudy = remoteData( { await: () => [this.queriedPhysicalStudies], @@ -5565,16 +5693,18 @@ export class StudyViewPageStore // this is used in page header(name and description) readonly displayedStudies = remoteData({ await: () => [ - this.filteredVirtualStudies, - this.filteredPhysicalStudies, - this.queriedPhysicalStudies, + this.filteredVirtualStudiesFromOriginalQuery, + this.filteredPhysicalStudiesFromOriginalQuery, + this.queriedPhysicalStudiesFromOriginalQuery, ], invoke: async () => { if ( - this.filteredPhysicalStudies.result.length === 0 && - this.filteredVirtualStudies.result.length === 1 + this.filteredPhysicalStudiesFromOriginalQuery.result.length === + 0 && + this.filteredVirtualStudiesFromOriginalQuery.result.length === 1 ) { - const virtualStudy = this.filteredVirtualStudies.result[0]; + const virtualStudy = this + .filteredVirtualStudiesFromOriginalQuery.result[0]; return [ { name: virtualStudy.data.name, @@ -5583,7 +5713,7 @@ export class StudyViewPageStore } as CancerStudy, ]; } else { - return this.queriedPhysicalStudies.result; + return this.queriedPhysicalStudiesFromOriginalQuery.result; } }, default: [], @@ -5746,7 +5876,7 @@ export class StudyViewPageStore readonly resourceDefinitions = remoteData({ await: () => [this.queriedPhysicalStudies], invoke: () => { - return internalClient.fetchResourceDefinitionsUsingPOST({ + return this.internalClient.fetchResourceDefinitionsUsingPOST({ studyIds: this.queriedPhysicalStudies.result.map( study => study.studyId ), @@ -5771,7 +5901,7 @@ export class StudyViewPageStore const promises = []; for (const resource of studyResourceDefinitions) { promises.push( - internalClient + this.internalClient .getAllStudyResourceDataInStudyUsingGET({ studyId: resource.studyId, resourceId: resource.resourceId, @@ -5826,12 +5956,15 @@ export class StudyViewPageStore }); readonly clinicalAttributes = remoteData({ - await: () => [this.queriedPhysicalStudyIds], + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], invoke: async () => { - if (this.queriedPhysicalStudyIds.result.length > 0) { + if ( + this.queriedPhysicalStudyIdsFromOriginalQuery.result.length > 0 + ) { return _.uniqBy( await defaultClient.fetchClinicalAttributesUsingPOST({ - studyIds: this.queriedPhysicalStudyIds.result, + studyIds: this.queriedPhysicalStudyIdsFromOriginalQuery + .result, }), clinicalAttribute => `${clinicalAttribute.patientAttribute}-${clinicalAttribute.clinicalAttributeId}` @@ -6683,6 +6816,16 @@ export class StudyViewPageStore this._chartVisibility, this.chartMetaSetForSummary ); + + // return [ { + // "uniqueKey": "msk_impact_2017_mutations", + // "dataType": "Genomic", + // "patientAttribute": false, + // "displayName": "Mutated Genes", + // "priority": 90, + // "renderWhenDataChange": false, + // "description": "" + // }] as ChartMeta[]; } @computed @@ -7723,7 +7866,7 @@ export class StudyViewPageStore attr => attr.attributeId ); - return internalClient.fetchClinicalDataCountsUsingPOST({ + return this.internalClient.fetchClinicalDataCountsUsingPOST({ clinicalDataCountFilter: { attributes, studyViewFilter: this.initialFilters, @@ -7754,7 +7897,7 @@ export class StudyViewPageStore >({ await: () => [this.initialVisibleAttributesClinicalDataBinAttributes], invoke: async () => { - const clinicalDataBinCountData = await internalClient.fetchClinicalDataBinCountsUsingPOST( + const clinicalDataBinCountData = await this.internalClient.fetchClinicalDataBinCountsUsingPOST( { dataBinMethod: 'STATIC', clinicalDataBinCountFilter: { @@ -8008,7 +8151,7 @@ export class StudyViewPageStore !_.isEmpty(studyViewFilter.sampleIdentifiers) || !_.isEmpty(studyViewFilter.studyIds) ) { - return internalClient.fetchFilteredSamplesUsingPOST({ + return this.internalClient.fetchFilteredSamplesUsingPOST({ studyViewFilter: studyViewFilter, }); } @@ -8128,7 +8271,7 @@ export class StudyViewPageStore )}` ); } - return internalClient.fetchFilteredSamplesUsingPOST({ + return this.internalClient.fetchFilteredSamplesUsingPOST({ studyViewFilter: this.filters, }); } else { @@ -8240,7 +8383,7 @@ export class StudyViewPageStore >( q => ({ invoke: async () => ({ - data: await internalClient.fetchClinicalDataViolinPlotsUsingPOST( + data: await this.internalClient.fetchClinicalDataViolinPlotsUsingPOST( { categoricalAttributeId: q.chartInfo.categoricalAttr.clinicalAttributeId, @@ -8332,7 +8475,7 @@ export class StudyViewPageStore ) { parameters.yAxisStart = 0; // mutation count always starts at 0 } - const result: any = await internalClient.fetchClinicalDataDensityPlotUsingPOST( + const result: any = await this.internalClient.fetchClinicalDataDensityPlotUsingPOST( parameters ); const bins = result.bins.filter( @@ -8392,7 +8535,7 @@ export class StudyViewPageStore : [this.mutationProfiles], invoke: async () => { if (!_.isEmpty(this.mutationProfiles.result)) { - let mutatedGenes = await internalClient.fetchMutatedGenesUsingPOST( + let mutatedGenes = await this.internalClient.fetchMutatedGenesUsingPOST( { studyViewFilter: this.filters, } @@ -8448,7 +8591,7 @@ export class StudyViewPageStore : [this.structuralVariantProfiles], invoke: async () => { if (!_.isEmpty(this.structuralVariantProfiles.result)) { - const structuralVariantGenes = await internalClient.fetchStructuralVariantGenesUsingPOST( + const structuralVariantGenes = await this.internalClient.fetchStructuralVariantGenesUsingPOST( { studyViewFilter: this.filters, } @@ -8504,7 +8647,7 @@ export class StudyViewPageStore : [this.structuralVariantProfiles], invoke: async () => { if (!_.isEmpty(this.structuralVariantProfiles.result)) { - const structuralVariantCounts = await internalClient.fetchStructuralVariantCountsUsingPOST( + const structuralVariantCounts = await this.internalClient.fetchStructuralVariantCountsUsingPOST( { studyViewFilter: this.filters, } @@ -8581,9 +8724,11 @@ export class StudyViewPageStore : [this.cnaProfiles], invoke: async () => { if (!_.isEmpty(this.cnaProfiles.result)) { - let cnaGenes = await internalClient.fetchCNAGenesUsingPOST({ - studyViewFilter: this.filters, - }); + let cnaGenes = await this.internalClient.fetchCNAGenesUsingPOST( + { + studyViewFilter: this.filters, + } + ); return cnaGenes.map(item => { return { ...item, @@ -8683,7 +8828,7 @@ export class StudyViewPageStore const molecularProfileIds = this.molecularProfiles.result.map( molecularProfile => molecularProfile.molecularProfileId ); - const report = await internalClient.fetchAlterationDriverAnnotationReportUsingPOST( + const report = await this.internalClient.fetchAlterationDriverAnnotationReportUsingPOST( { molecularProfileIds } ); return { @@ -9213,7 +9358,7 @@ export class StudyViewPageStore await: () => [this.molecularProfiles], invoke: async () => { const [counts, selectedSamples] = await Promise.all([ - internalClient.fetchMolecularProfileSampleCountsUsingPOST({ + this.internalClient.fetchMolecularProfileSampleCountsUsingPOST({ studyViewFilter: this.filters, }), toPromise(this.selectedSamples), @@ -9238,7 +9383,7 @@ export class StudyViewPageStore readonly caseListSampleCounts = remoteData({ invoke: async () => { const [counts, selectedSamples] = await Promise.all([ - internalClient.fetchCaseListCountsUsingPOST({ + this.internalClient.fetchCaseListCountsUsingPOST({ studyViewFilter: this.filters, }), toPromise(this.selectedSamples), @@ -9279,9 +9424,11 @@ export class StudyViewPageStore readonly initialMolecularProfileSampleCounts = remoteData({ invoke: async () => { - return internalClient.fetchMolecularProfileSampleCountsUsingPOST({ - studyViewFilter: this.initialFilters, - }); + return this.internalClient.fetchMolecularProfileSampleCountsUsingPOST( + { + studyViewFilter: this.initialFilters, + } + ); }, default: [], }); @@ -9342,7 +9489,7 @@ export class StudyViewPageStore let clinicalAttributeCountFilter = { sampleIdentifiers, } as ClinicalAttributeCountFilter; - return internalClient.getClinicalAttributeCountsUsingPOST({ + return this.internalClient.getClinicalAttributeCountsUsingPOST({ clinicalAttributeCountFilter, }); }, @@ -9421,9 +9568,7 @@ export class StudyViewPageStore } const calculateSampleCount = ( - result: - | (SampleTreatmentRow | PatientTreatmentRow)[] - | undefined + result: SampleTreatmentRow[] | undefined ) => { if (!result) { return 0; @@ -9439,34 +9584,34 @@ export class StudyViewPageStore }, new Set()).size; }; if (!_.isEmpty(this.sampleTreatments.result)) { - ret['SAMPLE_TREATMENTS'] = calculateSampleCount( - this.sampleTreatments.result - ); + ret[ + 'SAMPLE_TREATMENTS' + ] = this.sampleTreatments.result!.totalSamples; } if (!_.isEmpty(this.patientTreatments.result)) { - ret['PATIENT_TREATMENTS'] = calculateSampleCount( - this.patientTreatments.result - ); + ret[ + 'PATIENT_TREATMENTS' + ] = this.patientTreatments.result!.totalPatients; } if (!_.isEmpty(this.sampleTreatmentGroups.result)) { - ret['SAMPLE_TREATMENT_GROUPS'] = calculateSampleCount( - this.sampleTreatmentGroups.result - ); + ret[ + 'SAMPLE_TREATMENT_GROUPS' + ] = this.sampleTreatments.result!.totalSamples; } if (!_.isEmpty(this.patientTreatmentGroups.result)) { - ret['PATIENT_TREATMENT_GROUPS'] = calculateSampleCount( - this.patientTreatmentGroups.result - ); + ret[ + 'PATIENT_TREATMENT_GROUPS' + ] = this.patientTreatmentGroups.result!.totalPatients; } if (!_.isEmpty(this.sampleTreatmentTarget.result)) { - ret['SAMPLE_TREATMENT_TARGET'] = calculateSampleCount( - this.sampleTreatmentTarget.result - ); + ret[ + 'SAMPLE_TREATMENT_TARGET' + ] = this.sampleTreatments.result!.totalSamples; } if (!_.isEmpty(this.patientTreatmentTarget.result)) { - ret['PATIENT_TREATMENT_TARGET'] = calculateSampleCount( - this.patientTreatmentTarget.result - ); + ret[ + 'PATIENT_TREATMENT_TARGET' + ] = this.patientTreatmentTarget.result!.totalPatients; } if (!_.isEmpty(this.structuralVariantProfiles.result)) { const structVarGenesUniqueKey = getUniqueKeyFromMolecularProfileIds( @@ -9829,31 +9974,15 @@ export class StudyViewPageStore } readonly cancerStudiesData = remoteData({ - await: () => [this.selectedSamples], + await: () => [this.selectedSamplesFromOriginalQuery], invoke: async () => { // return empty if there are no filtered samples if (!this.hasFilteredSamples) { return []; } - let selectedSamples = []; - if ( - this._chartSampleIdentifiersFilterSet.has( - SpecialChartsUniqueKeyEnum.CANCER_STUDIES - ) - ) { - selectedSamples = await getSamplesByExcludingFiltersOnChart( - SpecialChartsUniqueKeyEnum.CANCER_STUDIES, - this.filters, - _.fromPairs(this._chartSampleIdentifiersFilterSet.toJSON()), - this.queriedSampleIdentifiers.result, - this.queriedPhysicalStudyIds.result - ); - } else { - selectedSamples = this.selectedSamples.result; - } const counts = _.values( _.reduce( - selectedSamples, + this.selectedSamplesFromOriginalQuery.result, (acc, sample) => { const studyId = sample.studyId; if (acc[studyId]) { @@ -9878,6 +10007,107 @@ export class StudyViewPageStore default: [], }); + readonly samplesFromOriginalQuery = remoteData({ + await: () => [ + // this.queriedSampleIdentifiers, + this.queriedPhysicalStudyIdsFromOriginalQuery, + ], + invoke: () => { + let studyViewFilter: StudyViewFilter = {} as any; + //this logic is need since fetchFilteredSamplesUsingPOST api accepts sampleIdentifiers or studyIds not both + // if (this.queriedSampleIdentifiers.result.length > 0) { + // studyViewFilter.sampleIdentifiers = this.queriedSampleIdentifiers.result; + // } else { + studyViewFilter.studyIds = this.queriedPhysicalStudyIdsFromOriginalQuery.result; + // } + + if ( + // !_.isEmpty(studyViewFilter.sampleIdentifiers) || + !_.isEmpty(studyViewFilter.studyIds) + ) { + return this.internalClient.fetchFilteredSamplesUsingPOST({ + studyViewFilter: studyViewFilter, + }); + } + return Promise.resolve([]); + }, + onError: () => {}, + default: [], + }); + + readonly molecularProfilesFromOriginalQuery = remoteData< + MolecularProfile[] + >({ + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], + invoke: async () => { + if ( + this.queriedPhysicalStudyIdsFromOriginalQuery.result.length > 0 + ) { + return await defaultClient.fetchMolecularProfilesUsingPOST({ + molecularProfileFilter: { + studyIds: this.queriedPhysicalStudyIdsFromOriginalQuery + .result, + } as MolecularProfileFilter, + }); + } + return []; + }, + onError: () => {}, + default: [], + }); + + readonly selectedSamplesFromOriginalQuery = remoteData({ + await: () => [ + this.samplesFromOriginalQuery, + this.molecularProfilesFromOriginalQuery, + this.queriedPhysicalStudyIdsFromOriginalQuery, + ], + invoke: () => { + //fetch samples when there are only filters applied + if (this.chartsAreFiltered) { + if (!this.hasSampleIdentifiersInFilter) { + return Promise.resolve([] as Sample[]); + } + // here we are validating only the molecular profile ids, + // but ideally we should validate the entire filters object + const invalidMolecularProfiles = findInvalidMolecularProfileIds( + this.filters, + this.molecularProfilesFromOriginalQuery.result + ); + if (invalidMolecularProfiles.length > 0) { + this.appStore.addError( + `Invalid molecular profile id(s): ${invalidMolecularProfiles.join( + ', ' + )}` + ); + } + return this.internalClient.fetchFilteredSamplesUsingPOST({ + studyViewFilter: { + ...this.filters, + studyIds: this.queriedPhysicalStudyIdsFromOriginalQuery + .result, + // TODO somehow find the missing relevant molecular profile ids from this.molecularProfilesFromOriginalQuery + // geneFilters: this.filters.geneFilters.map(gf => ({ + // ...gf, + // molecularProfileIds: [], + // })), + }, + }); + } else { + return Promise.resolve(this.samplesFromOriginalQuery.result); + } + }, + onError: () => {}, + default: [], + onResult: samples => { + if (samples.length === 0) { + this.blockLoading = true; + } else { + this.blockLoading = false; + } + }, + }); + @action showAsPieChart(uniqueKey: string, dataSize: number): void { if ( @@ -10050,7 +10280,7 @@ export class StudyViewPageStore } public readonly clinicalEventTypeCounts = remoteData({ invoke: async () => { - return internalClient.getClinicalEventTypeCountsUsingPOST({ + return this.internalClient.getClinicalEventTypeCountsUsingPOST({ studyViewFilter: this.filters, }); }, @@ -10058,15 +10288,17 @@ export class StudyViewPageStore // Poll ClinicalEventTypeCounts API with no filter to determine if table should be added to StudyView Page public readonly shouldDisplayClinicalEventTypeCounts = remoteData({ - await: () => [this.queriedPhysicalStudyIds], + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], invoke: async () => { const filters: Partial = {}; - filters.studyIds = this.queriedPhysicalStudyIds.result; + filters.studyIds = this.queriedPhysicalStudyIdsFromOriginalQuery.result; return Promise.resolve( ( - await internalClient.getClinicalEventTypeCountsUsingPOST({ - studyViewFilter: filters as StudyViewFilter, - }) + await this.internalClient.getClinicalEventTypeCountsUsingPOST( + { + studyViewFilter: filters as StudyViewFilter, + } + ) ).length > 0 ); }, @@ -10392,28 +10624,32 @@ export class StudyViewPageStore await: () => [this.shouldDisplaySampleTreatments], invoke: () => { if (this.shouldDisplaySampleTreatments.result) { - return internalClient.getAllSampleTreatmentsUsingPOST({ + return this.internalClient.fetchSampleTreatmentCountsUsing({ studyViewFilter: this.filters, }); } - return Promise.resolve([]); + return Promise.resolve(undefined); }, }); public readonly shouldDisplayPatientTreatments = remoteData({ - await: () => [this.queriedPhysicalStudyIds], + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], invoke: () => { - return internalClient.getContainsTreatmentDataUsingPOST({ - studyIds: toJS(this.queriedPhysicalStudyIds.result), + return this.internalClient.getContainsTreatmentDataUsingPOST({ + studyIds: toJS( + this.queriedPhysicalStudyIdsFromOriginalQuery.result + ), }); }, }); public readonly shouldDisplaySampleTreatments = remoteData({ - await: () => [this.queriedPhysicalStudyIds], + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], invoke: () => { - return internalClient.getContainsSampleTreatmentDataUsingPOST({ - studyIds: toJS(this.queriedPhysicalStudyIds.result), + return this.internalClient.getContainsSampleTreatmentDataUsingPOST({ + studyIds: toJS( + this.queriedPhysicalStudyIdsFromOriginalQuery.result + ), }); }, }); @@ -10423,12 +10659,15 @@ export class StudyViewPageStore public readonly patientTreatments = remoteData({ await: () => [this.shouldDisplayPatientTreatments], invoke: () => { - if (this.shouldDisplayPatientTreatments.result) { - return internalClient.getAllPatientTreatmentsUsingPOST({ - studyViewFilter: this.filters, - }); - } - return Promise.resolve([]); + return this.internalClient.fetchPatientTreatmentCountsUsing({ + studyViewFilter: this.filters, + }); + // + // return this.internalClient.getAllPatientTreatmentsUsingPOST({ + // studyViewFilter: this.filters, + // }); + + //return Promise.resolve({}); }, }); @@ -10436,7 +10675,7 @@ export class StudyViewPageStore await: () => [this.shouldDisplaySampleTreatmentGroups], invoke: () => { if (this.shouldDisplaySampleTreatmentGroups.result) { - return internalClient.getAllSampleTreatmentsUsingPOST({ + return this.internalClient.getAllSampleTreatmentsUsingPOST({ studyViewFilter: this.filters, tier: 'AgentClass', }); @@ -10446,26 +10685,30 @@ export class StudyViewPageStore }); public readonly shouldDisplayPatientTreatmentGroups = remoteData({ - await: () => [this.queriedPhysicalStudyIds], + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], invoke: () => { if (!getServerConfig().enable_treatment_groups) { return Promise.resolve(false); } - return internalClient.getContainsTreatmentDataUsingPOST({ - studyIds: toJS(this.queriedPhysicalStudyIds.result), + return this.internalClient.getContainsTreatmentDataUsingPOST({ + studyIds: toJS( + this.queriedPhysicalStudyIdsFromOriginalQuery.result + ), tier: 'AgentClass', }); }, }); public readonly shouldDisplaySampleTreatmentGroups = remoteData({ - await: () => [this.queriedPhysicalStudyIds], + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], invoke: () => { if (!getServerConfig().enable_treatment_groups) { return Promise.resolve(false); } - return internalClient.getContainsSampleTreatmentDataUsingPOST({ - studyIds: toJS(this.queriedPhysicalStudyIds.result), + return this.internalClient.getContainsSampleTreatmentDataUsingPOST({ + studyIds: toJS( + this.queriedPhysicalStudyIdsFromOriginalQuery.result + ), tier: 'AgentClass', }); }, @@ -10477,12 +10720,12 @@ export class StudyViewPageStore await: () => [this.shouldDisplayPatientTreatmentGroups], invoke: () => { if (this.shouldDisplayPatientTreatmentGroups.result) { - return internalClient.getAllPatientTreatmentsUsingPOST({ + return this.internalClient.fetchPatientTreatmentCountsUsing({ studyViewFilter: this.filters, tier: 'AgentClass', }); } - return Promise.resolve([]); + return Promise.resolve(undefined); }, }); @@ -10490,7 +10733,7 @@ export class StudyViewPageStore await: () => [this.shouldDisplaySampleTreatmentTarget], invoke: () => { if (this.shouldDisplaySampleTreatmentTarget.result) { - return internalClient.getAllSampleTreatmentsUsingPOST({ + return this.internalClient.getAllSampleTreatmentsUsingPOST({ studyViewFilter: this.filters, tier: 'AgentTarget', }); @@ -10500,26 +10743,30 @@ export class StudyViewPageStore }); public readonly shouldDisplayPatientTreatmentTarget = remoteData({ - await: () => [this.queriedPhysicalStudyIds], + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], invoke: () => { if (!getServerConfig().enable_treatment_groups) { return Promise.resolve(false); } - return internalClient.getContainsTreatmentDataUsingPOST({ - studyIds: toJS(this.queriedPhysicalStudyIds.result), + return this.internalClient.getContainsTreatmentDataUsingPOST({ + studyIds: toJS( + this.queriedPhysicalStudyIdsFromOriginalQuery.result + ), tier: 'AgentTarget', }); }, }); public readonly shouldDisplaySampleTreatmentTarget = remoteData({ - await: () => [this.queriedPhysicalStudyIds], + await: () => [this.queriedPhysicalStudyIdsFromOriginalQuery], invoke: () => { if (!getServerConfig().enable_treatment_groups) { return Promise.resolve(false); } - return internalClient.getContainsSampleTreatmentDataUsingPOST({ - studyIds: toJS(this.queriedPhysicalStudyIds.result), + return this.internalClient.getContainsSampleTreatmentDataUsingPOST({ + studyIds: toJS( + this.queriedPhysicalStudyIdsFromOriginalQuery.result + ), tier: 'AgentTarget', }); }, @@ -10530,13 +10777,10 @@ export class StudyViewPageStore public readonly patientTreatmentTarget = remoteData({ await: () => [this.shouldDisplayPatientTreatmentTarget], invoke: () => { - if (this.shouldDisplayPatientTreatmentTarget.result) { - return internalClient.getAllPatientTreatmentsUsingPOST({ - studyViewFilter: this.filters, - tier: 'AgentTarget', - }); - } - return Promise.resolve([]); + return this.internalClient.fetchPatientTreatmentCountsUsing({ + studyViewFilter: this.filters, + tier: 'AgentTarget', + }); }, }); @@ -11061,14 +11305,16 @@ export class StudyViewPageStore // createStructuralVariantQuery(sv, this.plotsSelectedGenes.result!) // ); - return await internalClient.fetchStructuralVariantsUsingPOST({ - structuralVariantFilter: { - entrezGeneIds, - structuralVariantQueries: [], - sampleMolecularIdentifiers, - molecularProfileIds: [], - }, - }); + return await this.internalClient.fetchStructuralVariantsUsingPOST( + { + structuralVariantFilter: { + entrezGeneIds, + structuralVariantQueries: [], + sampleMolecularIdentifiers, + molecularProfileIds: [], + }, + } + ); } }, }); @@ -11368,7 +11614,7 @@ export class StudyViewPageStore readonly genesets = remoteData({ invoke: () => { if (this.genesetIds && this.genesetIds.length > 0) { - return internalClient.fetchGenesetsUsingPOST({ + return this.internalClient.fetchGenesetsUsingPOST({ genesetIds: this.genesetIds.slice(), }); } else { @@ -11414,7 +11660,7 @@ export class StudyViewPageStore if (_.isEmpty(filters)) { return []; } else { - return internalClient.fetchStructuralVariantsUsingPOST({ + return this.internalClient.fetchStructuralVariantsUsingPOST({ structuralVariantFilter: { entrezGeneIds: [q.entrezGeneId], sampleMolecularIdentifiers: filters, diff --git a/src/pages/studyView/StudyViewPageTabs.ts b/src/pages/studyView/StudyViewPageTabs.ts index bc76f0292d6..f70e5e445f0 100644 --- a/src/pages/studyView/StudyViewPageTabs.ts +++ b/src/pages/studyView/StudyViewPageTabs.ts @@ -1,5 +1,6 @@ export enum StudyViewPageTabKeyEnum { SUMMARY = 'summary', + SUMMARY_COLUMN_STORE = 'summaryColumnStore', CLINICAL_DATA = 'clinicalData', HEATMAPS = 'heatmaps', CN_SEGMENTS = 'cnSegments', diff --git a/src/pages/studyView/StudyViewQueryExtractor.ts b/src/pages/studyView/StudyViewQueryExtractor.ts index c1555b9af9a..013ffb82ce1 100644 --- a/src/pages/studyView/StudyViewQueryExtractor.ts +++ b/src/pages/studyView/StudyViewQueryExtractor.ts @@ -1,4 +1,4 @@ -import { toJS } from 'mobx'; +import { observable, toJS } from 'mobx'; import { StudyViewPageStore, StudyViewURLQuery } from './StudyViewPageStore'; import _ from 'lodash'; import { @@ -32,9 +32,9 @@ export class StudyIdQueryExtractor implements StudyViewQueryExtractor { query.studyId ?? query.cancer_study_id ?? query.id ?? ''; if (studyIdsString) { studyIds = studyIdsString.trim().split(','); - if (!_.isEqual(studyIds, toJS(store.studyIds))) { + if (!_.isEqual(studyIds, toJS(store._studyIds))) { // update if different - store.studyIds = studyIds; + store._studyIds = studyIds; } } } diff --git a/src/pages/studyView/StudyViewUtils.tsx b/src/pages/studyView/StudyViewUtils.tsx index e2e8b78bc78..a9faf19ac1b 100644 --- a/src/pages/studyView/StudyViewUtils.tsx +++ b/src/pages/studyView/StudyViewUtils.tsx @@ -25,10 +25,12 @@ import { NumericGeneMolecularData, Patient, PatientIdentifier, + PatientTreatmentReport, PatientTreatmentRow, Sample, SampleClinicalDataCollection, SampleIdentifier, + SampleTreatmentReport, SampleTreatmentRow, StructuralVariantFilterQuery, StudyViewFilter, @@ -47,7 +49,9 @@ import { } from './StudyViewPageStore'; import { StudyViewPageTabKeyEnum } from 'pages/studyView/StudyViewPageTabs'; import { Layout } from 'react-grid-layout'; -import internalClient from 'shared/api/cbioportalInternalClientInstance'; +import internalClient, { + getInteralClient, +} from 'shared/api/cbioportalInternalClientInstance'; import defaultClient from 'shared/api/cbioportalClientInstance'; import client from 'shared/api/cbioportalClientInstance'; import { @@ -2539,7 +2543,7 @@ export function getSamplesByExcludingFiltersOnChart( updatedFilter.sampleIdentifiers = queriedSampleIdentifiers; } } - return internalClient.fetchFilteredSamplesUsingPOST({ + return getInteralClient().fetchFilteredSamplesUsingPOST({ studyViewFilter: updatedFilter, }); } @@ -3182,7 +3186,7 @@ export async function getAllClinicalDataByStudyViewFilter( const [remoteClinicalDataCollection, totalItems]: [ SampleClinicalDataCollection, number - ] = await internalClient + ] = await getInteralClient() .fetchClinicalDataClinicalTableUsingPOSTWithHttpInfo({ studyViewFilter, pageSize: pageSize | 500, @@ -4073,7 +4077,7 @@ export async function invokeGenericAssayDataCount( chartInfo: GenericAssayChart, filters: StudyViewFilter ) { - const result: GenericAssayDataCountItem[] = await internalClient.fetchGenericAssayDataCountsUsingPOST( + const result: GenericAssayDataCountItem[] = await getInteralClient().fetchGenericAssayDataCountsUsingPOST( { genericAssayDataCountFilter: { genericAssayDataFilters: [ @@ -4135,12 +4139,16 @@ export async function invokeGenomicDataCount( projection: 'SUMMARY', }, }; - result = await internalClient.fetchMutationDataCountsUsingPOST(params); + result = await getInteralClient().fetchMutationDataCountsUsingPOST( + params + ); getDisplayedValue = transformMutatedType; getDisplayedColor = (value: string) => getMutationColorByCategorization(transformMutatedType(value)); } else { - result = await internalClient.fetchGenomicDataCountsUsingPOST(params); + result = await getInteralClient().fetchGenomicDataCountsUsingPOST( + params + ); getDisplayedValue = getCNAByAlteration; getDisplayedColor = (value: string | number) => getCNAColorByAlteration(getCNAByAlteration(value)); @@ -4194,7 +4202,7 @@ export async function invokeMutationDataCount( }, } as any; - const result = await internalClient.fetchMutationDataCountsUsingPOST( + const result = await getInteralClient().fetchMutationDataCountsUsingPOST( params ); @@ -4536,26 +4544,29 @@ export async function getGenesCNADownloadData( } export async function getPatientTreatmentDownloadData( - promise: MobxPromise + promise: MobxPromise ): Promise { if (promise.result) { const header = ['Treatment', '#']; let data = [header.join('\t')]; - _.each(promise.result, (record: PatientTreatmentRow) => { - let rowData = [record.treatment, record.count]; - data.push(rowData.join('\t')); - }); + _.each( + promise.result.patientTreatments, + (record: PatientTreatmentRow) => { + let rowData = [record.treatment, record.count]; + data.push(rowData.join('\t')); + } + ); return data.join('\n'); } else return ''; } export async function getSampleTreatmentDownloadData( - promise: MobxPromise + promise: MobxPromise ): Promise { if (promise.result) { const header = ['Treatment', 'Pre/Post', '#']; let data = [header.join('\t')]; - _.each(promise.result, (record: SampleTreatmentRow) => { + _.each(promise.result.treatments, (record: SampleTreatmentRow) => { let rowData = [record.treatment, record.time, record.count]; data.push(rowData.join('\t')); }); diff --git a/src/pages/studyView/rfc80Tester.tsx b/src/pages/studyView/rfc80Tester.tsx new file mode 100644 index 00000000000..5e3f4951515 --- /dev/null +++ b/src/pages/studyView/rfc80Tester.tsx @@ -0,0 +1,175 @@ +import * as React from 'react'; +import _ from 'lodash'; +import { useCallback, useEffect } from 'react'; +import axios from 'axios'; +import { + reportValidationResult, + runSpecs, + validate, +} from 'shared/api/validation.ts'; +import { getBrowserWindow } from 'cbioportal-frontend-commons'; +import { observer } from 'mobx-react'; +import { useLocalObservable } from 'mobx-react-lite'; +import { SAVE_TEST_KEY } from 'shared/api/testMaker'; + +const CACHE_KEY: string = 'testCache'; + +const RFC_TEST_SHOW: string = 'RFC_TEST_SHOW'; + +const LIVE_VALIDATE_KEY: string = 'LIVE_VALIDATE_KEY'; + +function getCache() { + return getBrowserWindow()[CACHE_KEY] || {}; + //return localStorage.getItem(CACHE_KEY); +} + +function clearCache() { + getBrowserWindow()[CACHE_KEY] = {}; +} + +export const RFC80Test = observer(function() { + const store = useLocalObservable(() => ({ + tests: [], + show: !!localStorage.getItem(RFC_TEST_SHOW), + listening: !!localStorage.getItem(SAVE_TEST_KEY), + validate: !!localStorage.getItem(LIVE_VALIDATE_KEY), + })); + + const clearCacheCallback = useCallback(() => { + clearCache(); + }, []); + + const toggleListener = useCallback(() => { + store.listening = !store.listening; + if (getBrowserWindow().localStorage.getItem(SAVE_TEST_KEY)) { + getBrowserWindow().localStorage.removeItem(SAVE_TEST_KEY); + } else { + getBrowserWindow().localStorage.setItem(SAVE_TEST_KEY, 'true'); + } + }, []); + + const toggleShow = useCallback(() => { + !!localStorage.getItem(RFC_TEST_SHOW) + ? localStorage.removeItem(RFC_TEST_SHOW) + : localStorage.setItem(RFC_TEST_SHOW, 'true'); + store.show = !store.show; + }, []); + + const toggleLiveValidate = useCallback(() => { + !!localStorage.getItem(LIVE_VALIDATE_KEY) + ? localStorage.removeItem(LIVE_VALIDATE_KEY) + : localStorage.setItem(LIVE_VALIDATE_KEY, 'true'); + store.validate = !store.validate; + }, []); + + const runTests = useCallback(async () => { + let json = []; + + try { + json = await $.getJSON( + 'https://localhost:3000/common/merged-tests.json' + ); + } catch (ex) { + alert('merged-tests.json not found'); + } + + const fileFilter = $('#apiTestFilter') + .val() + ?.toString(); + + const files: any[] = fileFilter?.trim().length + ? json.filter((f: any) => new RegExp(fileFilter).test(f.file)) + : json; + + await runSpecs(files, axios, '', 'verbose'); + }, []); + + useEffect(() => { + if (getCache()) { + const tests = getCache(); + const parsed = _.values(tests).map((j: any) => j); + store.tests = parsed; + } + + const checker = setInterval(() => { + if (getCache()) { + const tests = getCache(); + const parsed = _.values(tests); + store.tests = parsed; + } else { + store.tests = []; + } + }, 1000); + + return () => { + clearInterval(checker); + }; + }, []); + + const txt = ` + { + "name":"", + "note":"", + "studies":[], + "tests":[ + ${store.tests.map((t: any) => JSON.stringify(t)).join(',\n\n')} + ] + }`; + + if (!store.show) { + return ( +
+ +
+ ); + } + + return ( +
+ + + + + + + { + + } +
+ ); +}); diff --git a/src/pages/studyView/table/treatments/PatientTreatmentsTable.tsx b/src/pages/studyView/table/treatments/PatientTreatmentsTable.tsx index 8a5a335c95a..4c9cc9c1f2e 100644 --- a/src/pages/studyView/table/treatments/PatientTreatmentsTable.tsx +++ b/src/pages/studyView/table/treatments/PatientTreatmentsTable.tsx @@ -7,7 +7,10 @@ import { Column, SortDirection, } from '../../../../shared/components/lazyMobXTable/LazyMobXTable'; -import { PatientTreatmentRow } from 'cbioportal-ts-api-client'; +import { + PatientTreatmentReport, + PatientTreatment, +} from 'cbioportal-ts-api-client'; import { correctColumnWidth } from 'pages/studyView/StudyViewUtils'; import LabeledCheckbox from 'shared/components/labeledCheckbox/LabeledCheckbox'; import styles from 'pages/studyView/table/tables.module.scss'; @@ -36,7 +39,7 @@ export type PatientTreatmentsTableColumn = { export type PatientTreatmentsTableProps = { tableType: TreatmentTableType; - promise: MobxPromise; + promise: MobxPromise; width: number; height: number; filters: string[][]; @@ -59,9 +62,7 @@ const DEFAULT_COLUMN_WIDTH_RATIO: { [PatientTreatmentsTableColumnKey.COUNT]: 0.2, }; -class MultiSelectionTableComponent extends FixedHeaderTable< - PatientTreatmentRow -> {} +class MultiSelectionTableComponent extends FixedHeaderTable {} @observer export class PatientTreatmentsTable extends TreatmentsTable< @@ -80,7 +81,7 @@ export class PatientTreatmentsTable extends TreatmentsTable< } createNubmerColumnCell( - row: PatientTreatmentRow, + row: PatientTreatment, cellMargin: number ): JSX.Element { return ( @@ -111,9 +112,7 @@ export class PatientTreatmentsTable extends TreatmentsTable< cellMargin: number ) => { const defaults: { - [key in PatientTreatmentsTableColumnKey]: Column< - PatientTreatmentRow - >; + [key in PatientTreatmentsTableColumnKey]: Column; } = { [PatientTreatmentsTableColumnKey.TREATMENT]: { name: columnKey, @@ -123,10 +122,10 @@ export class PatientTreatmentsTable extends TreatmentsTable< headerName={columnKey} /> ), - render: (data: PatientTreatmentRow) => ( + render: (data: PatientTreatment) => ( ), - sortBy: (data: PatientTreatmentRow) => data.treatment, + sortBy: (data: PatientTreatment) => data.treatment, defaultSortDirection: 'asc' as 'asc', filter: filterTreatmentCell, width: columnWidth, @@ -140,9 +139,9 @@ export class PatientTreatmentsTable extends TreatmentsTable< headerName={columnKey} /> ), - render: (data: PatientTreatmentRow) => + render: (data: PatientTreatment) => this.createNubmerColumnCell(data, 28), - sortBy: (data: PatientTreatmentRow) => + sortBy: (data: PatientTreatment) => data.count + toNumericValue(data.treatment), defaultSortDirection: 'desc' as 'desc', filter: filterTreatmentCell, @@ -181,8 +180,8 @@ export class PatientTreatmentsTable extends TreatmentsTable< ); } - @computed get tableData(): PatientTreatmentRow[] { - return this.props.promise.result || []; + @computed get tableData(): PatientTreatment[] { + return this.props.promise.result?.patientTreatments || []; } @computed @@ -206,7 +205,7 @@ export class PatientTreatmentsTable extends TreatmentsTable< .filter(data => this.flattenedFilters.includes(treatmentUniqueKey(data)) ) - .sortBy(data => + .sortBy(data => ifNotDefined( order[treatmentUniqueKey(data)], Number.POSITIVE_INFINITY diff --git a/src/pages/studyView/table/treatments/SampleTreatmentsTable.tsx b/src/pages/studyView/table/treatments/SampleTreatmentsTable.tsx index 43070757afe..025fff9d7b8 100644 --- a/src/pages/studyView/table/treatments/SampleTreatmentsTable.tsx +++ b/src/pages/studyView/table/treatments/SampleTreatmentsTable.tsx @@ -7,7 +7,10 @@ import { Column, SortDirection, } from '../../../../shared/components/lazyMobXTable/LazyMobXTable'; -import { SampleTreatmentRow } from 'cbioportal-ts-api-client'; +import { + SampleTreatmentReport, + SampleTreatmentRow, +} from 'cbioportal-ts-api-client'; import { correctColumnWidth } from 'pages/studyView/StudyViewUtils'; import LabeledCheckbox from 'shared/components/labeledCheckbox/LabeledCheckbox'; import styles from 'pages/studyView/table/tables.module.scss'; @@ -37,7 +40,7 @@ export type SampleTreatmentsTableColumn = { export type SampleTreatmentsTableProps = { tableType: TreatmentTableType; - promise: MobxPromise; + promise: MobxPromise; width: number; height: number; filters: string[][]; @@ -214,7 +217,7 @@ export class SampleTreatmentsTable extends TreatmentsTable< } @computed get tableData(): SampleTreatmentRow[] { - return this.props.promise.result || []; + return this.props.promise.result?.treatments || []; } @computed get selectableTableData() { diff --git a/src/pages/studyView/table/treatments/treatmentsTableUtil.tsx b/src/pages/studyView/table/treatments/treatmentsTableUtil.tsx index d18e2694bf4..af38c602b92 100644 --- a/src/pages/studyView/table/treatments/treatmentsTableUtil.tsx +++ b/src/pages/studyView/table/treatments/treatmentsTableUtil.tsx @@ -3,6 +3,7 @@ import { SampleTreatmentFilter, PatientTreatmentFilter, PatientTreatmentRow, + PatientTreatment, } from 'cbioportal-ts-api-client'; import { ChartMeta } from 'pages/studyView/StudyViewUtils'; import styles from 'pages/studyView/table/tables.module.scss'; @@ -90,7 +91,7 @@ export const TreatmentGenericColumnHeader = class GenericColumnHeader extends Re }; export const TreatmentColumnCell = class TreatmentColumnCell extends React.Component< - { row: PatientTreatmentRow | SampleTreatmentRow }, + { row: PatientTreatment | SampleTreatmentRow }, {} > { render() { @@ -99,7 +100,7 @@ export const TreatmentColumnCell = class TreatmentColumnCell extends React.Compo }; export function filterTreatmentCell( - cell: PatientTreatmentRow | SampleTreatmentRow, + cell: PatientTreatment | SampleTreatmentRow, filter: string ): boolean { return cell.treatment.toUpperCase().includes(filter.toUpperCase()); diff --git a/src/shared/api/cbioportalInternalClientInstance.ts b/src/shared/api/cbioportalInternalClientInstance.ts index 0aa9e2f3c57..efe2de7ed8c 100644 --- a/src/shared/api/cbioportalInternalClientInstance.ts +++ b/src/shared/api/cbioportalInternalClientInstance.ts @@ -1,5 +1,148 @@ import { CBioPortalAPIInternal } from 'cbioportal-ts-api-client'; +import { getLoadConfig } from 'config/config'; +import { getBrowserWindow, hashString } from 'cbioportal-frontend-commons'; +import { toJS } from 'mobx'; +import { reportValidationResult, validate } from 'shared/api/validation'; +import _ from 'lodash'; +import { makeTest, urlChopper } from 'shared/api/testMaker'; +import axios from 'axios'; + +// function invokeValidation(func){ +// getBrowserWindow().invokeCache = getBrowserWindow().invokeCache || []; +// +// getBrowserWindow().invokeCache.push(func); +// +// +// +// } + +function proxyColumnStore(client: any, endpoint: string) { + if (getBrowserWindow().location.search.includes('legacy')) { + return; + } + + const method = endpoint.match( + new RegExp('fetchPatientTreatmentCounts|fetchSampleTreatmentCounts') + ) + ? `${endpoint}UsingWithHttpInfo` + : `${endpoint}UsingPOSTWithHttpInfo`; + const old = client[method]; + + client[method] = function(params: any) { + const host = getLoadConfig().baseUrl; + + const oldRequest = this.request; + + const endpoints = [ + 'ClinicalDataCounts', + 'MutatedGenes', + 'CaseList', + 'ClinicalDataBin', + 'MolecularProfileSample', + 'CNAGenes', + 'StructuralVariantGenes', + 'FilteredSamples', + 'ClinicalDataDensity', + 'MutationDataCounts', + 'PatientTreatmentCounts', + 'SampleTreatmentCounts', + 'GenomicData', + 'GenericAssay', + 'ViolinPlots', + 'ClinicalEventTypeCounts', + ]; + + const matchedMethod = method.match(new RegExp(endpoints.join('|'))); + if (localStorage.getItem('LIVE_VALIDATE_KEY') && matchedMethod) { + this.request = function(...origArgs: any[]) { + const params = toJS(arguments[2]); + + const oldSuccess = arguments[7]; + + arguments[7] = function(response: any) { + const url = + origArgs[1].replace( + /column-store\/api/, + 'column-store' + ) + + '?' + + _.map(origArgs[4], (v, k) => `${k}=${v}&`).join(''); + + setTimeout(() => { + makeTest(params, urlChopper(url), matchedMethod[0]); + }, 1000); + + const hash = hashString( + JSON.stringify({ data: params, url: urlChopper(url) }) + ); + + validate( + axios, + url, + params, + matchedMethod[0], + hash, + response.body + ).then((result: any) => { + reportValidationResult(result, 'LIVE', 'verbose'); + }); + + return oldSuccess.apply(this, arguments); + }; + + oldRequest.apply(this, arguments); + }; + } + + params.$domain = method.match( + new RegExp('PatientTreatmentCounts|SampleTreatmentCounts') + ) + ? `//${host}` + : `//${host}/api/column-store`; + const url = old.apply(this, [params]); + + this.request = oldRequest; + + return url; + }; +} const internalClient = new CBioPortalAPIInternal(); +export const internalClientColumnStore = new CBioPortalAPIInternal(); + +const oldRequest = (internalClientColumnStore as any).request; +(internalClientColumnStore as any).request = function(...args: any) { + args[1] = args[1].replace(/column-store\/api/, 'column-store'); + return oldRequest.apply(this, args); +}; + +proxyColumnStore(internalClientColumnStore, 'fetchCNAGenes'); +proxyColumnStore(internalClientColumnStore, 'fetchStructuralVariantGenes'); +proxyColumnStore(internalClientColumnStore, 'fetchCaseListCounts'); +proxyColumnStore( + internalClientColumnStore, + 'fetchMolecularProfileSampleCounts' +); +proxyColumnStore(internalClientColumnStore, 'fetchMutatedGenes'); +proxyColumnStore(internalClientColumnStore, 'fetchFilteredSamples'); +proxyColumnStore(internalClientColumnStore, 'fetchClinicalDataCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchClinicalDataBinCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchClinicalDataDensityPlot'); +proxyColumnStore(internalClientColumnStore, 'fetchMutationDataCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchPatientTreatmentCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchSampleTreatmentCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchClinicalDataDensityPlot'); +proxyColumnStore(internalClientColumnStore, 'getClinicalEventTypeCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchMutationDataCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchGenomicDataCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchGenomicDataBinCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchGenericAssayDataBinCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchGenericAssayDataCounts'); +proxyColumnStore(internalClientColumnStore, 'fetchClinicalDataViolinPlots'); + export default internalClient; + +export function getInteralClient() { + return internalClientColumnStore; +} diff --git a/src/shared/api/testMaker.ts b/src/shared/api/testMaker.ts new file mode 100644 index 00000000000..758b157808d --- /dev/null +++ b/src/shared/api/testMaker.ts @@ -0,0 +1,80 @@ +import { getBrowserWindow, hashString } from 'cbioportal-frontend-commons'; +import { toJS } from 'mobx'; +import _ from 'lodash'; + +export const SAVE_TEST_KEY = 'save_test_enabled'; + +export function urlChopper(url: string) { + try { + if (typeof url === 'string') { + return url.match(/[^\/]*\/\/[^\/]*(\/.*)/)![1]; + } else { + return url; + } + } catch (ex) { + return url; + } +} + +export async function makeTest(data: any, url: string, label: string) { + const hash = hashString(JSON.stringify({ data, url: urlChopper(url) })); + + const filterString = $('.userSelections') + .find('*') + .contents() + .filter(function() { + return this.nodeType === 3; + }) + .toArray() + .map(n => n.textContent) + .slice(0, -1) + .reduce((acc, s) => { + switch (s) { + case null: + acc += ''; + break; + case '(': + acc += ' ('; + break; + case ')': + acc += ') '; + break; + case 'or': + acc += ' OR '; + break; + case 'and': + acc += ' AND '; + break; + default: + acc += s || ''; + break; + } + return acc; + }, ''); + + const entry = { + hash, + filterString, + data, + url, + label, + studies: toJS(getBrowserWindow().studyViewPageStore.studyIds), + filterUrl: urlChopper( + getBrowserWindow().studyPage.studyViewFullUrlWithFilter + ), + }; + + if (getBrowserWindow().localStorage.getItem(SAVE_TEST_KEY)) + saveTest(hash, entry); + + return entry; +} + +function saveTest(hash: number, entry: any) { + const testCache = getBrowserWindow().testCache || {}; + + if (!(hash in testCache)) { + testCache[hash] = entry; + getBrowserWindow().testCache = testCache; + } +} diff --git a/src/shared/api/validation.ts b/src/shared/api/validation.ts new file mode 100644 index 00000000000..67edd38697e --- /dev/null +++ b/src/shared/api/validation.ts @@ -0,0 +1,619 @@ +export const isObject = (value: any) => { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + !(value instanceof RegExp) && + !(value instanceof Date) && + !(value instanceof Set) && + !(value instanceof Map) + ); +}; + +export function dynamicSortSingle(property: string) { + var sortOrder = 1; + if (property[0] === '-') { + sortOrder = -1; + property = property.substr(1); + } + return function(a: any, b: any) { + /* next line works with strings and numbers, + * and you may want to customize it to your needs + */ + var result = + a[property] < b[property] ? -1 : a[property] > b[property] ? 1 : 0; + return result * sortOrder; + }; +} + +export function dynamicSort(property: string[]) { + if (property.length === 1) { + return dynamicSortSingle(property[0]); + } else { + const prop1 = property[0]; + const prop2 = property[1]; + return function(a: any, b: any) { + /* next line works with strings and numbers, + * and you may want to customize it to your needs + */ + let af = a[prop1]; + let bf = b[prop1]; + let as = a[prop2]; + let bs = b[prop2]; + + // If first value is same + if (af == bf) { + return as < bs ? -1 : as > bs ? 1 : 0; + } else { + return af < bf ? -1 : 1; + } + }; + } +} + +export function getArrays(inp: any, output: Array) { + if (inp instanceof Array) { + output.push(inp); + inp.forEach(n => getArrays(n, output)); + } else if (isObject(inp)) { + for (const k in inp) { + if (/\d\.\d{10,}$/.test(inp[k])) { + try { + inp[k] = inp[k].toFixed(5); + } catch (ex) {} + } + } + + if (inp.counts) { + inp.counts = inp.counts.filter((n: any) => { + return n.label != 'NA'; + }); + } + + // this is get rid if extraneouys properties that conflict + delete inp.matchingGenePanelIds; + delete inp.cytoband; + delete inp.numberOfProfiledCases; + + Object.values(inp).forEach(nn => getArrays(nn, output)); + } + return output; +} + +const deleteFields: Record = { + MolecularProfileSampleCounts: ['label'], + CaseList: ['label'], + SampleListsCounts: ['label'], + CnaGenes: ['qValue'], + MutatedGenes: ['qValue'], +}; + +const sortFields: Record = { + ClinicalDataBinCounts: 'attributeId,specialValue', + ClinicalDataBin: 'attributeId,specialValue', + FilteredSamples: 'studyId,patientId,sampleId', + SampleTreatmentCounts: 'treatment,time', + PatientTreatmentCounts: 'treatment', + ClinicalDataCounts: 'attributeId,value', + ClinicalDataTypeCounts: 'eventType', + ClinicalEventTypeCounts: 'eventType', +}; + +function getLegacyPatientTreatmentCountUrl(url: string) { + return url.replace( + /api\/treatments\/patient-counts\/fetch?/, + 'api/treatments/patient' + ); +} + +function getLegacySampleTreatmentCountUrl(url: string) { + return url.replace( + /api\/treatments\/sample-counts\/fetch?/, + 'api/treatments/sample' + ); +} + +const treatmentLegacyUrl: Record string> = { + PatientTreatmentCounts: getLegacyPatientTreatmentCountUrl, + SampleTreatmentCounts: getLegacySampleTreatmentCountUrl, +}; + +const treatmentConverter: Record any> = { + PatientTreatmentCounts: convertLegacyPatientTreatmentCountsToCh, + SampleTreatmentCounts: convertLegacySampleTreatmentCountsToCh, +}; + +function convertLegacySampleTreatmentCountsToCh(legacyData: any) { + const sampleIdSet = new Set(); + const treatments: Array<{ + time: string; + treatment: string; + count: number; + samples: Array; + }> = []; + + legacyData.forEach((legacySampleTreatment: any) => { + let treatment = { + count: legacySampleTreatment['count'], + samples: new Array(), + time: legacySampleTreatment['time'], + treatment: legacySampleTreatment['treatment'], + }; + + treatments.push(treatment); + const samples = legacySampleTreatment['samples']; + if (samples instanceof Array) { + samples.forEach(sample => { + sampleIdSet.add(sample['sampleId']); + }); + } + }); + return { + totalSamples: sampleIdSet.size, + treatments: treatments, + }; +} + +function convertLegacyPatientTreatmentCountsToCh(legacyData: any) { + const patientIdSet = new Set(); + const treatments: Array<{ treatment: string; count: number }> = []; + + legacyData.forEach((legacyTreatment: any) => { + let treatment = { + count: legacyTreatment['count'], + treatment: legacyTreatment['treatment'], + }; + treatments.push(treatment); + + const samples = legacyTreatment['samples']; + if (samples instanceof Array) { + samples.forEach(sample => { + patientIdSet.add(sample['patientId']); + }); + } + }); + + return { + totalPatients: patientIdSet.size, + totalSamples: 0, + patientTreatments: treatments, + }; +} + +export function deepSort(inp: any, label: string) { + const arrs = getArrays(inp, []); + + arrs.forEach(arr => { + if (label in deleteFields) { + arr.forEach((m: any) => { + deleteFields[label].forEach(l => { + delete m[l]; + }); + }); + } + + arr.forEach((m: any) => { + if (m.value && m.value.toLowerCase) m.value = m.value.toLowerCase(); + }); + + arr.forEach((m: any) => { + if (m.specialValue && m.specialValue.toLowerCase) + m.specialValue = m.specialValue.toLowerCase(); + }); + + if (!arr.length) return; + if (!isObject(arr[0])) { + arr.sort(); + } else { + // it's an array of objects + + // this is going to make sure the keys in the objects + // are in a sorted order + arr.forEach((o: any) => { + Object.keys(o) + .sort() + .forEach(k => { + const val = o[k]; + delete o[k]; + o[k] = val; + }); + }); + + if (sortFields[label]) { + attemptSort(sortFields[label].split(','), arr); + } else { + const fields = [ + 'attributeId', + 'value', + 'hugoGeneSymbol', + 'uniqueSampleKey', + 'alteration', + ]; + fields.forEach(f => attemptSort([f], arr)); + } + } + }); + + return inp; +} + +function attemptSort(keys: string[], arr: any) { + arr.sort(dynamicSort(keys)); +} + +let win: any; + +try { + win = window; +} catch (ex) { + win = {}; +} + +function removeElement(nums: any[], val: any) { + for (let i = 0; i < nums.length; i++) { + if (nums[i] === val) { + nums.splice(i, 1); + i--; + } + } +} + +export function compareCounts(clData: any, legacyData: any, label: string) { + // @ts-ignore + let clDataClone = win.structuredClone ? structuredClone(clData) : clData; + + let legacyDataClone = win.structuredClone + ? // @ts-ignore + structuredClone(legacyData) + : legacyData; + + // get trid of duplicates + //clDataClone = filterDuplicates(clDataClone); + + var clDataSorted = deepSort(clDataClone, label); + var legacyDataSorted = deepSort(legacyDataClone, label); + + getArrays(clDataSorted, []).forEach((arr: any) => { + arr.filter((n: any) => /NA/i.test(n.value)).forEach((val: any) => { + removeElement(arr, val); + }); + }); + + getArrays(legacyDataSorted, []).forEach((arr: any) => { + arr.filter((n: any) => /NA/i.test(n.value)).forEach((val: any) => { + removeElement(arr, val); + }); + }); + + // get rid of these little guys + if (clDataSorted && clDataSorted.filter) + clDataSorted = clDataSorted.filter((n: any) => n.specialValue != 'NA'); + + if (legacyDataSorted && legacyDataSorted.filter) + legacyDataSorted = legacyDataSorted.filter( + (n: any) => n.specialValue != 'NA' + ); + + if (treatmentConverter[label]) { + legacyDataSorted = treatmentConverter[label](legacyDataSorted); + } + const result = + JSON.stringify(clDataSorted) === JSON.stringify(legacyDataSorted); + + return { + clDataSorted, + legacyDataSorted, + status: result, + label, + }; +} + +export async function validate( + ajax: any, + url: string, + params: any, + label: string, + hash: number, + body?: any, + elapsedTime: any = 0, + assertResponse: any[] | undefined = undefined, + onFail: (...args: any[]) => void = () => {} +) { + let chXHR: any; + + let chResult; + let legacyResult; + + if (body) { + chResult = { body, elapsedTime, status: 200 }; + } else { + chResult = await ajax + .post(url, params) + .then(function(response: any) { + return { + status: response.status, + body: response.data, + elapsedTime: response.headers['elapsed-time'], + }; + }) + .catch(function(error: any) { + return { + body: null, + error, + elapsedTime: null, + status: error.status, + }; + }); + } + + if (assertResponse) { + legacyResult = assertResponse; + } else { + let legacyUrl = url.replace(/column-store\//, ''); + + if (treatmentLegacyUrl[label]) { + legacyUrl = treatmentLegacyUrl[label](legacyUrl); + } + + legacyResult = await ajax + .post(legacyUrl, params) + .then(function(response: any) { + return { + status: response.status, + body: response.data, + elapsedTime: response.headers['elapsed-time'], + }; + }) + .catch(function(error: any) { + return { + body: null, + error, + elapsedTime: null, + status: error.status, + }; + }); + } + + const result: any = compareCounts(chResult.body, legacyResult.body, label); + result.url = url; + result.hash = hash; + result.data = params; + result.chDuration = chResult.elapsedTime; + result.legacyDuration = !assertResponse && legacyResult.elapsedTime; + result.chError = chResult.error; + + if (!result.status) { + onFail(url); + } + + return result; +} + +const red = '\x1b[31m'; +const green = '\x1b[32m'; +const blue = '\x1b[36m'; +const reset = '\x1b[0m'; + +export function reportValidationResult( + result: any, + prefix = '', + logLevel = '' +) { + const skipMessage = + result.test && result.test.skip ? `(SKIPPED ${result.test.skip})` : ''; + + const errorStatus = result.chError ? `(${result.chError.status})` : ''; + + const data = result.data || result?.test.data; + const studies = (data.studyIds || data.studyViewFilter.studyIds).join(','); + + !result.status && + !result.supressed && + console.groupCollapsed( + `${red} ${prefix} ${result.label} (${result.hash}) ${skipMessage} failed (${errorStatus}) ${studies} :( ${reset}` + ); + + if (result.supressed) { + console.log( + `${blue} ${prefix} ${result.label} (${result.hash}) ${skipMessage} SUPPRESSED :( ${reset}` + ); + } + + if (logLevel === 'verbose' && !result.status) { + console.log('failed test', { + url: result.url, + test: result.test, + studies: result?.test?.studies, + legacyDuration: result.legacyDuration, + chDuration: result.chDuration, + equal: result.status, + httpError: result.httpError, + }); + } + + if (result.status) { + console.log( + `${prefix} ${result.label} (${result.hash}) passed :) ch: ${ + result.chDuration + } legacy: ${result.legacyDuration && result.legacyDuration}` + ); + } + + if (!result.status && logLevel == 'verbose') { + if (result?.clDataSorted?.length && result?.legacyDataSorted?.length) { + for (var i = 0; i < result?.clDataSorted?.length; i++) { + const cl = result.clDataSorted[i]; + if ( + JSON.stringify(cl) !== + JSON.stringify(result.legacyDataSorted[i]) + ) { + console.groupCollapsed( + `First invalid item (${result.label})` + ); + console.log('Clickhouse:', cl); + console.log('Legacy:', result.legacyDataSorted[i]); + console.groupEnd(); + break; + } + } + } + console.groupCollapsed('All Data'); + console.log( + `CH: ${result?.clDataSorted?.length}, Legacy:${result?.legacyDataSorted?.length}` + ); + console.log('legacy', result.legacyDataSorted); + console.log('CH', result.clDataSorted); + console.groupEnd(); + } + + !result.status && console.groupEnd(); +} + +export async function runSpecs( + files: any, + axios: any, + host: string = '', + logLevel = '', + onFail: any = () => {}, + supressors: any = [] +) { + // @ts-ignore + const allTests = files + // @ts-ignore + .flatMap((n: any) => n.suites) + // @ts-ignore + .flatMap((n: any) => n.tests); + + const totalCount = allTests.length; + + const onlyDetected = allTests.some((t: any) => t.only === true); + + console.log(`Running specs (${files.length} of ${totalCount})`); + + if (logLevel === 'verbose') { + console.groupCollapsed('specs'); + //console.log('raw', json); + console.log('filtered', files); + console.groupEnd(); + } + + let place = 0; + let errors: any[] = []; + let skips: any[] = []; + let passed: any[] = []; + let httpErrors: any[] = []; + let supressed: any[] = []; + + const invokers: (() => Promise)[] = [] as any; + files + .map((f: any) => f.suites) + .forEach((suite: any) => { + suite.forEach((col: any) => + col.tests.forEach((test: any) => { + test.url = test.url.replace( + /column-store\/api/, + 'column-store' + ); + + if (!onlyDetected || test.only) { + invokers.push( + // @ts-ignore + () => { + return validate( + axios, + host + test.url, + test.data, + test.label, + test.hash, + undefined, + undefined, + test.assertResponse + ).then((report: any) => { + if (!report.status) { + onFail(test, report); + } + + report.test = test; + place = place + 1; + const prefix = `${place} of ${totalCount}`; + if (report instanceof Promise) { + report.then((report: any) => { + if (test?.skip) { + skips.push(test.hash); + } else if (!report.status) { + report.httpError + ? httpErrors.push(test.hash) + : errors.push(test.hash); + } else if (report.status) + passed.push(test.hash); + + reportValidationResult( + report, + prefix, + logLevel + ); + }); + } else { + if (test?.skip) { + skips.push(test.hash); + } else if (!report.status) { + let supress = []; + + supress = supressors + .map((f: any) => { + try { + return f(report); + } catch (exc) { + return false; + } + }) + .filter((r: any) => r); + + if (supress.length) { + supressed.push(test.hash); + report.supressed = true; + } else { + report.httpError + ? httpErrors.push(test.hash) + : errors.push(test.hash); + } + } else if (report.status) + passed.push(test.hash); + + reportValidationResult( + report, + prefix, + logLevel + ); + } + }); + } + ); + } + }) + ); + }); + + const concurrent = 10; + const batches = Math.ceil(invokers.length / concurrent); + + for (var i = 0; i < batches; i++) { + const proms = []; + for (const inv of invokers.slice( + i * concurrent, + (i + 1) * concurrent + )) { + proms.push(inv()); + } + await Promise.all(proms); + } + + console.group('FINAL REPORT'); + console.log(`PASSED: ${passed.length} of ${totalCount}`); + console.log(`FAILED: ${errors.length} (${errors.join(',')})`); + console.log(`HTTP ERRORS: ${httpErrors.length} (${httpErrors.join(',')})`); + console.log(`SKIPPED: ${skips.length} (${skips.join(',')})`); + console.log(`SUPRESSED: ${supressed.length} (${supressed.join(',')})`); + console.groupEnd(); + // console.groupEnd(); +} diff --git a/webpack.config.js b/webpack.config.js index 3580b03db73..9434fb440b6 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,6 +6,8 @@ var ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); var TerserPlugin = require('terser-webpack-plugin'); var { TypedCssModulesPlugin } = require('typed-css-modules-webpack-plugin'); +const fsProm = require('fs/promises'); + var commit = '"unknown"'; var version = '"unknown"'; // Don't show COMMIT/VERSION on Heroku (crashes, because no git dir) @@ -41,6 +43,9 @@ const dotenv = require('dotenv'); const webpack = require('webpack'); const path = require('path'); +const { watch } = require('fs'); +const fs = require('fs/promises'); +const { mergeApiTestJson } = require('./api-e2e/mergeJson'); const join = path.join; const resolve = path.resolve; @@ -170,6 +175,7 @@ var config = { { from: './common-dist', to: 'reactapp' }, { from: './src/rootImages', to: 'images' }, { from: './src/common', to: 'common' }, + { from: './api-e2e/json', to: 'common' }, { from: './src/globalStyles/prefixed-bootstrap.min.css', to: 'reactapp/prefixed-bootstrap.min.css', diff --git a/yarn.lock b/yarn.lock index 840732ddb91..f254a2c2041 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1135,6 +1135,18 @@ dependencies: commander "^2.15.1" +"@clickhouse/client-common@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-1.6.0.tgz#371a64592f55758ac7ddca707a30b78cfc656f84" + integrity sha512-DPdfPUIrBumWff8JdE2oLqnOIN1DwxXt48juV+tB6zaonufQR1QImtUGEhIFIqRWJU7zErtQ1eKfkwjLY0MYDg== + +"@clickhouse/client@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-1.6.0.tgz#703d7b4aef35c60987db9e8f8f4420da1f93adc3" + integrity sha512-cjuOL5O11Y/axYIvSaNgZjZqD53BBbScrG0DCgZ7XBCz5KWlA5lIG3fP6A4jME277q8ZUYAQxUOlJjeF18872A== + dependencies: + "@clickhouse/client-common" "1.6.0" + "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" @@ -5411,6 +5423,15 @@ aws4@^1.2.1, aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-cli@^6.4.5: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1" @@ -6920,6 +6941,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -8941,6 +8973,15 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== +csvtojson@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/csvtojson/-/csvtojson-2.0.10.tgz#11e7242cc630da54efce7958a45f443210357574" + integrity sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ== + dependencies: + bluebird "^3.5.1" + lodash "^4.17.3" + strip-bom "^2.0.0" + cubic-hermite@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cubic-hermite/-/cubic-hermite-1.0.0.tgz#84e3b2f272b31454e8393b99bb6aed45168c14e5" @@ -9463,6 +9504,15 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -10268,6 +10318,18 @@ es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.5.0, es-abstract@^1.5.1 is-regex "^1.0.4" object-keys "^1.0.12" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^0.9.0: version "0.9.1" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.1.tgz#f203bf394a630a552d381acf01a17ef08843b140" @@ -11211,6 +11273,11 @@ follow-redirects@^1.0.0: dependencies: debug "^3.2.6" +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + font-atlas-sdf@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/font-atlas-sdf/-/font-atlas-sdf-1.3.3.tgz#8323f136c69d73a235aa8c6ada640e58f180b8c0" @@ -11313,6 +11380,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" @@ -11322,6 +11398,11 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +format-curl@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/format-curl/-/format-curl-2.2.1.tgz#06c68beb7568616dfb4a62e7f935449064845ecb" + integrity sha512-6w/KmhD9wjBdqRezh9zXUHcCtJeaaqtz3li1R/2yEmLnFlj7C06+B0r30tyRKQ0siwoRjk/JRi6P3IUaeSBObA== + formidable@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" @@ -11476,6 +11557,11 @@ function-bind@^1.0.2, function-bind@^1.1.1, function-bind@~1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327" @@ -11611,6 +11697,17 @@ get-intrinsic@^1.0.2: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-own-enumerable-property-symbols@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b" @@ -12431,6 +12528,13 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" @@ -12544,6 +12648,18 @@ has-passive-events@^1.0.0: dependencies: is-browser "^2.0.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -12554,6 +12670,11 @@ has-symbols@^1.0.1: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -12613,6 +12734,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hast-to-hyperscript@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz#3decd7cb4654bca8883f6fcbd4fb3695628c4296" @@ -14598,6 +14726,11 @@ jest@^27.2.1: import-local "^3.0.2" jest-cli "^27.2.1" +jquery-deferred@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/jquery-deferred/-/jquery-deferred-0.3.1.tgz#596eca1caaff54f61b110962b23cafea74c35355" + integrity sha512-YTzoTYR/yrjmNh6B6exK7lC1jlDazEzt9ZlZvdRscv+I1AJqN1SmU3ZAn4iMGiVhwAavCrbijDVyTc0lmr9ZCA== + jquery-migrate@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/jquery-migrate/-/jquery-migrate-3.0.0.tgz#03f320596fe1a1db09cdcfb74b14571994aad7bb" @@ -15510,7 +15643,7 @@ lodash.words@^3.0.0: dependencies: lodash._root "^3.0.0" -lodash@4.x, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -16928,6 +17061,15 @@ mz@^2.5.0: object-assign "^4.0.1" thenify-all "^1.0.0" +najax@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/najax/-/najax-1.0.7.tgz#706dce52d4b738dce01aee97f392ccdb79d51eef" + integrity sha512-JqBMguf2plv1IDqhOE6eebnTivjS/ej0C/Sw831jVc+dRQIMK37oyktdQCGAQtwpl5DikOWI2xGfIlBPSSLgXg== + dependencies: + jquery-deferred "^0.3.0" + lodash "^4.17.21" + qs "^6.2.0" + nan@^2.12.1: version "2.13.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" @@ -17477,6 +17619,11 @@ object-inspect@^1.1.0, object-inspect@~1.6.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + object-inspect@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" @@ -19663,6 +19810,11 @@ proxy-addr@~2.0.4, proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.0" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -19772,6 +19924,13 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.2.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + qs@^6.3.0, qs@^6.5.1: version "6.6.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.6.0.tgz#a99c0f69a8d26bf7ef012f871cdabb0aee4424c2" @@ -22165,6 +22324,18 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + set-immediate-shim@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" @@ -22307,6 +22478,16 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + sigmund@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"