Skip to content

Commit ac065c0

Browse files
committed
Merge branch 'development' into 3.2.0
2 parents 2fa143b + 8ceb61f commit ac065c0

File tree

11 files changed

+4180
-247
lines changed

11 files changed

+4180
-247
lines changed

src/client/features/enrichment/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ class Enrichment extends React.Component {
4848
let { pathways } = await ServerAPI.enrichmentAPI({ query: sources}, 'analysis');
4949
let enrichmentNetwork = await ServerAPI.enrichmentAPI({ pathways }, 'visualization');
5050
let networkHasZeroNodes = enrichmentNetwork.graph.elements.nodes.length === 0;
51+
enrichmentNetwork.graph.elements.nodes.forEach( node => {
52+
const intersection = _.intersection( node.data.geneSet, sources );
53+
_.assign( node.data, { intersection } );
54+
});
5155

5256
cy.remove('*');
5357
cy.add({

src/server/external-services/gprofiler/gconvert/index.js

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
const _ = require('lodash');
2-
const qs = require('query-string');
32
const QuickLRU = require('quick-lru');
43
const logger = require('../../../logger');
54

65
const { fetch } = require('../../../../util');
7-
const cleanUpEntrez = require('../clean-up-entrez');
86
const InvalidParamError = require('../../../../server/errors/invalid-param');
97
const { GPROFILER_URL, PC_CACHE_MAX_SIZE, NS_HGNC, NS_HGNC_SYMBOL, NS_UNIPROT, NS_NCBI_GENE, NS_ENSEMBL } = require('../../../../config');
108
const { cachePromise } = require('../../../cache');
11-
const GCONVERT_URL = GPROFILER_URL + 'gconvert.cgi';
9+
const GCONVERT_URL = GPROFILER_URL + 'api/convert/convert/';
1210

1311
const GPROFILER_NS_MAP = new Map([
1412
[NS_HGNC, 'HGNC_ACC'],
@@ -18,14 +16,16 @@ const GPROFILER_NS_MAP = new Map([
1816
[NS_ENSEMBL, 'ENSG']
1917
]);
2018

19+
const toJSON = res => res.json();
20+
2121
// create gconvert opts for a gconvert request
2222
// validates params
2323
const createGConvertOpts = opts => {
24+
// see https://biit.cs.ut.ee/gprofiler/page/apis
2425
const defaults = {
25-
output: 'mini',
2626
organism: 'hsapiens',
2727
target: NS_HGNC,
28-
prefix: 'ENTREZGENE_ACC'
28+
numeric_ns: 'ENTREZGENE_ACC'
2929
};
3030

3131
let gConvertOpts = _.assign({}, defaults, opts);
@@ -34,34 +34,30 @@ const createGConvertOpts = opts => {
3434
if( !Array.isArray( query ) ){
3535
throw new InvalidParamError( `Error creating gconvert request - expected an array of strings for "query", got ${query}`);
3636
}
37-
let gconvertQuery = query.join(' ');
3837
let gconvertTarget = GPROFILER_NS_MAP.get( target );
3938

4039
if( gconvertTarget == null ){
4140
throw new InvalidParamError( `Error creating gconvert request - expected a valid "targetDb", got ${target}`);
4241
}
4342

44-
gConvertOpts.query = gconvertQuery;
4543
gConvertOpts.target = gconvertTarget;
4644

4745
return gConvertOpts;
4846
};
4947

50-
const gConvertResponseHandler = body => {
51-
let entityInfoList = body.split('\n').map( ele => ele.split('\t') ).filter( ele => ele != '');
48+
const gConvertResponseHandler = json => {
49+
let entityInfoList = _.get( json, ['result'] );
5250
let unrecognized = new Set();
5351
let duplicate = {};
5452
let entityMap = new Map();
5553
let alias = {};
56-
const INITIAL_ALIAS_INDEX = 1;
57-
const CONVERTED_ALIAS_INDEX = 3;
5854

5955
entityInfoList.forEach( entityInfo => {
60-
let convertedAlias = entityInfo[CONVERTED_ALIAS_INDEX];
61-
let initialAlias = cleanUpEntrez(entityInfo[INITIAL_ALIAS_INDEX]);
56+
let convertedAlias = entityInfo.converted === 'None' ? null : entityInfo.converted;
57+
let initialAlias = entityInfo.incoming;
6258

63-
if( convertedAlias === 'N/A' ){
64-
unrecognized.add(initialAlias);
59+
if( _.isNull( convertedAlias ) ){
60+
unrecognized.add( initialAlias );
6561
return;
6662
}
6763

@@ -100,9 +96,10 @@ const rawValidatorGconvert = ( query, opts = {} ) => {
10096
.then( () => createGConvertOpts( _.assign(opts, { query }) ) )
10197
.then( gconvertOpts => fetch( GCONVERT_URL, {
10298
method: 'post',
103-
body: qs.stringify( gconvertOpts )
99+
body: JSON.stringify( gconvertOpts ),
100+
headers: { 'Content-Type': 'application/json' }
104101
}))
105-
.then( response => response.text() )
102+
.then( toJSON )
106103
.then( gConvertResponseHandler )
107104
.catch( err => {
108105
logger.error(`Error in validatorGconvert - ${err.message}`);
Lines changed: 68 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,102 @@
11
const { fetch } = require('../../../util');
22
const _ = require('lodash');
3-
const qs = require('query-string');
43
const QuickLRU = require('quick-lru');
54

6-
const cleanUpEntrez = require('./clean-up-entrez');
75
const { GPROFILER_URL } = require('../../../config');
86
const logger = require('../../logger');
97
const { cachePromise } = require('../../cache');
108

11-
const GPROFILER_GOST_URL = GPROFILER_URL + 'index.cgi';
9+
const toJSON = res => res.json();
10+
11+
// See https://biit.cs.ut.ee/gprofiler/page/apis
12+
const GPROFILER_GOST_URL = GPROFILER_URL + 'api/gost/profile/';
1213
const GPROFILER_DEFAULT_OPTS = {
13-
output: 'mini',
1414
organism: 'hsapiens',
15-
significant: 0,
16-
sort_by_structure: 1,
17-
ordered_query: 0,
18-
as_ranges: 0,
19-
no_iea: 1,
20-
underrep: 0,
21-
hierfiltering: 'none',
22-
user_thr: 0.05,
23-
min_set_size: 5,
24-
max_set_size: 200,
25-
threshold_algo: 'analytical',
26-
domain_size_type: 'annotated',
27-
custbg: [],
28-
'sf_GO:BP': 1,
29-
sf_REAC: 1,
30-
prefix: 'ENTREZGENE_ACC'
15+
sources: ['GO:BP', 'REAC'],
16+
user_threshold: 0.05,
17+
all_results: false,
18+
ordered: false,
19+
combined: false,
20+
measure_underrepresentation: false,
21+
no_iea: true,
22+
domain_scope: 'annotated',
23+
numeric_ns: 'ENTREZGENE_ACC',
24+
significance_threshold_method: 'g_SCS',
25+
background: [],
26+
no_evidences: true
3127
};
3228

33-
// parseGProfilerResponse(gProfilerResponse) takes the text response
34-
// from gProfiler gProfilerResponse and parses it into JSON format
35-
const parseGProfilerResponse = gProfilerResponse => {
36-
let lines = gProfilerResponse.split('\n').map( line => {
37-
if( line.substring(0, 1) === '#' ){ return ''; }
38-
return line;
39-
});
40-
41-
let elements = _.compact(lines).map( line => line.split('\t') );
29+
const DEFAULT_FILTER_OPTS = {
30+
minSetSize: 5,
31+
maxSetSize: 200
32+
};
4233

34+
const parseGProfilerResponse = ( json, opts ) => {
4335
let pathways = [];
44-
let P_VALUE_INDEX = 2;
45-
let PATHWAY_ID_INDEX = 8;
46-
let DESCRIPTION_INDEX = 11;
47-
let GENE_INTERSECTION_LIST_INDEX = 13;
48-
49-
50-
elements.forEach( ele => {
51-
let pathwayId = ele[PATHWAY_ID_INDEX];
52-
let pValue = ele[P_VALUE_INDEX];
53-
let description = ele[DESCRIPTION_INDEX].trim();
54-
let geneIntersectionList = ele[GENE_INTERSECTION_LIST_INDEX].split(',').map( gene => cleanUpEntrez( gene ) );
55-
36+
const {
37+
minSetSize = DEFAULT_FILTER_OPTS.minSetSize,
38+
maxSetSize = DEFAULT_FILTER_OPTS.maxSetSize
39+
} = opts;
40+
const pathwayInfoList = _.get( json, ['result'] );
41+
pathwayInfoList.forEach( pathwayInfo => {
42+
let { native: id, name, p_value, term_size } = pathwayInfo;
43+
if( term_size < minSetSize || term_size > maxSetSize ) return;
5644
pathways.push({
57-
id: pathwayId,
58-
data: {
59-
name: description,
60-
p_value: pValue,
61-
intersection: geneIntersectionList
62-
}
45+
id,
46+
data: { name, p_value }
6347
});
6448
});
6549

6650
return { pathways };
6751
};
6852

6953

54+
const validateParams = ( query, opts ) => {
55+
let error = null,
56+
message = '';
57+
const { minSetSize, maxSetSize } = opts;
58+
59+
if( !Array.isArray( query ) ) message = 'ERROR: query must be an array';
60+
if( minSetSize && (typeof( minSetSize ) != 'number' || minSetSize < 0) ) message = 'ERROR: minSetSize must be a positive number';
61+
if( maxSetSize && (typeof( maxSetSize ) != 'number' || maxSetSize < 0) ) message = 'ERROR: maxSetSize must be a positive number';
62+
if( (minSetSize && maxSetSize) && (maxSetSize < minSetSize) ) message = 'ERROR: minSetSize must be less than maxSetSize';
63+
64+
if ( !_.isEmpty( message ) ) error = new Error( message );
65+
return error;
66+
};
67+
68+
const getGProfilerOpts = query => _.assign( {}, GPROFILER_DEFAULT_OPTS, { query } );
69+
7070
// enrichmemt(query, opts) takes a list of gene identifiers query
7171
// and an object of user settings opts
7272
// and extracts enrichment information
7373
// from g:Profiler for the query list based on userSetting
74-
const rawEnrichment = (query, opts) => {
75-
return new Promise((resolve, reject) => {
76-
let {
77-
minSetSize = GPROFILER_DEFAULT_OPTS.min_set_size,
78-
maxSetSize = GPROFILER_DEFAULT_OPTS.max_set_size,
79-
background = []
80-
} = opts;
81-
82-
if (!Array.isArray(query)) {
83-
reject(new Error('ERROR: genes should be an array'));
84-
}
85-
if( typeof(minSetSize) != 'number' ) {
86-
reject(new Error('ERROR: minSetSize should be a number'));
87-
}
88-
if( minSetSize < 0 ){
89-
reject(new Error('ERROR: minSetSize should be >= 0'));
90-
}
91-
if( typeof(maxSetSize) != 'number' ){
92-
reject(new Error('ERROR: maxSetSize should be a number'));
93-
}
94-
if( maxSetSize < minSetSize ){
95-
reject(new Error('ERROR: maxSetSize should be >= minSetSize'));
96-
}
97-
if( !Array.isArray(background) ){
98-
reject(new Error('ERROR: backgroundGenes should be an array'));
99-
}
100-
101-
let gProfilerOpts = _.assign( {}, GPROFILER_DEFAULT_OPTS, {
102-
query: query.sort().join(' '),
103-
min_set_size: minSetSize,
104-
max_set_size: maxSetSize,
105-
custbg: background.join(' ')
106-
} );
107-
108-
109-
fetch(GPROFILER_GOST_URL, { method: 'post', body: qs.stringify(gProfilerOpts)})
110-
.then( res => res.text() )
111-
.then( gprofilerRes => parseGProfilerResponse( gprofilerRes ) )
112-
.then( pathwayInfo => resolve( pathwayInfo ) )
113-
.catch( err =>{
114-
logger.error(`Error in rawEnrichment - ${err.message}`);
115-
throw err;
116-
});
117-
});
74+
const rawEnrichment = ( query, opts ) => {
75+
const gProfilerOpts = getGProfilerOpts( query, opts );
76+
77+
return fetch( GPROFILER_GOST_URL, {
78+
method: 'post',
79+
body: JSON.stringify( gProfilerOpts ),
80+
headers: { 'Content-Type': 'application/json' }
81+
})
82+
.then( toJSON )
83+
.then( gprofilerRes => parseGProfilerResponse( gprofilerRes, opts ) )
84+
.catch( err =>{
85+
logger.error(`Error in rawEnrichment - ${err.message}`);
86+
throw err;
87+
});
11888
};
11989

90+
12091
const lruCache = new QuickLRU({ maxSize: 100 });
12192

122-
const enrichment = cachePromise(rawEnrichment, lruCache);
93+
const enrichmentWrapper = cachePromise( rawEnrichment, lruCache );
12394

95+
const enrichment = async ( query, opts ) => {
96+
const paramError = validateParams( query, opts );
97+
if( paramError ) throw paramError;
12498

99+
return enrichmentWrapper( query.sort(), opts );
100+
};
125101

126102
module.exports = { enrichment, parseGProfilerResponse };

src/server/routes/enrichment/index.js

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,10 @@ enrichmentRouter.post('/validation', (req, res, next) => {
122122
* schema:
123123
* "$ref": "#/definitions/success/analysisSuccess"
124124
* '400':
125-
* description: Invalid input (minSetSize, maxSetSize, backgroundGenes or JSON format)
125+
* description: Invalid input (minSetSize, maxSetSize, or JSON format)
126126
* schema:
127127
* "$ref": "#/definitions/error/analysisError"
128+
*
128129
*/
129130
// expose a rest endpoint for enrichment service
130131
enrichmentRouter.post('/analysis', (req, res, next) => {
@@ -222,7 +223,7 @@ enrichmentRouter.post('/visualization', (req, res, next) => {
222223
* analysisObj:
223224
* type: object
224225
* required:
225-
* - genes
226+
* - query
226227
* properties:
227228
* query:
228229
* type: array
@@ -239,14 +240,7 @@ enrichmentRouter.post('/visualization', (req, res, next) => {
239240
* type: number
240241
* description: "maximum size of functional category, larger categories are
241242
* excluded \n default: 200"
242-
* example: 400
243-
* background:
244-
* type: array
245-
* description: "Biological identifiers used
246-
* as a custom statistical background \n default: []"
247-
* example: []
248-
* items:
249-
* type: string
243+
* example: 400
250244
* visualizationObj:
251245
* type: object
252246
* required:
@@ -328,11 +322,6 @@ enrichmentRouter.post('/visualization', (req, res, next) => {
328322
* name:
329323
* type: string
330324
* example: DNA-templated transcription, elongation
331-
* intersection:
332-
* type: array
333-
* items:
334-
* type: string
335-
* example: PAF1
336325
* visualizationSuccess:
337326
* type: object
338327
* required:

test/server/enrichment/analysis/analysis-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ const PARSED_GPROFILER = require('./parsed-gprofiler.json');
88
describe ('Enrichment service: analysis', function () {
99
describe ('Test parseGProfilerResponse()', () => {
1010
it ('should return to a correct object', () => {
11-
const gGostbodytxt = fs.readFileSync( path.resolve( __dirname, 'ggost-body.txt' ), 'utf-8' );
12-
const result = parseGProfilerResponse( gGostbodytxt );
11+
const gGostbodyJSON = require('./ggost-body.json');
12+
const result = parseGProfilerResponse( gGostbodyJSON, {} );
1313
expect ( result ).to.deep.equal ( PARSED_GPROFILER );
1414
});
1515
});

0 commit comments

Comments
 (0)