gramene-search 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gramene-search",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "search wrapper for gramene",
5
5
  "source": "src/index.js",
6
6
  "main": "dist/index.js",
@@ -2,10 +2,27 @@ import { createAsyncResourceBundle, createSelector } from 'redux-bundler';
2
2
  import overlay from '../fieldCatalog.overlay.json';
3
3
  import { study_info as VEP_STUDY_INFO } from '../vepStudyInfo';
4
4
 
5
- const SAMPLE_QUERY = 'capabilities:expression';
6
- const SAMPLE_ROWS = 500;
7
- const DIFFEXPR_FL = 'id,*_pval_attr_f,*_logfc_attr_f,*_l2fc_attr_f';
8
- const DIFFEXPR_ROWS_PER_TAXON = 50;
5
+ // Fields matched by these globs are multi-valued in Solr.
6
+ const MULTIVALUED_PATTERNS = [
7
+ /__expr$/,
8
+ /__ancestors$/,
9
+ /__xrefs$/,
10
+ /^VEP__/,
11
+ /^homology__/
12
+ ];
13
+
14
+ function parseCsvHeader(text) {
15
+ if (!text) return [];
16
+ const line = text.split(/\r?\n/, 1)[0] || '';
17
+ return line
18
+ .split(',')
19
+ .map(s => s.trim().replace(/^"|"$/g, ''))
20
+ .filter(Boolean);
21
+ }
22
+
23
+ function synthesizedValue(name) {
24
+ return MULTIVALUED_PATTERNS.some(re => re.test(name)) ? [0] : 0;
25
+ }
9
26
 
10
27
  function applyTemplate(tpl, match) {
11
28
  if (!tpl) return null;
@@ -66,43 +83,14 @@ function inferType(value) {
66
83
  return typeof value;
67
84
  }
68
85
 
69
- function extractSwaggerFields(swagger) {
70
- const defs = (swagger && swagger.definitions) || {};
71
- const out = {};
72
- const collect = (schemaName, prefix = '') => {
73
- const def = defs[schemaName];
74
- if (!def || !def.properties) return;
75
- for (const [prop, spec] of Object.entries(def.properties)) {
76
- const fullName = prefix ? `${prefix}.${prop}` : prop;
77
- let type = spec.type || 'object';
78
- if (spec.$ref) type = 'ref:' + spec.$ref.split('/').pop();
79
- if (spec.type === 'array') {
80
- const inner = spec.items || {};
81
- type = 'array<' + (inner.type || (inner.$ref ? inner.$ref.split('/').pop() : 'unknown')) + '>';
82
- }
83
- out[fullName] = {
84
- name: fullName,
85
- type,
86
- description: spec.description || '',
87
- source: 'swagger'
88
- };
89
- }
90
- };
91
- collect('Result');
92
- collect('GeneDocument');
93
- return out;
94
- }
95
-
96
- function buildCatalog(swagger, sampleDocs) {
86
+ function buildCatalog(sampleDocs) {
97
87
  const hidden = new Set(overlay.hidden || []);
98
88
  const overlayFields = overlay.fields || {};
99
89
  const patterns = overlay.patterns || [];
100
90
  const groupsMeta = overlay.groups || {};
101
91
 
102
- const swaggerFields = extractSwaggerFields(swagger);
103
-
104
- // Union keys across sample docs + swagger keys
105
- const keys = new Set(Object.keys(swaggerFields));
92
+ // Union keys from overlay.fields (hardcoded catalog entries) and sample docs.
93
+ const keys = new Set(Object.keys(overlayFields));
106
94
  const sampleValues = {};
107
95
  for (const doc of (sampleDocs || [])) {
108
96
  for (const [k, v] of Object.entries(doc)) {
@@ -120,11 +108,10 @@ function buildCatalog(swagger, sampleDocs) {
120
108
  if (classified && classified.isHidden) continue;
121
109
  const sampleVal = sampleValues[name];
122
110
  const sampleType = sampleVal !== undefined ? inferType(sampleVal) : null;
123
- const swaggerEntry = swaggerFields[name];
124
111
 
125
112
  const group = (overlayEntry && overlayEntry.group)
126
113
  || (classified && classified.group)
127
- || (swaggerEntry ? 'core' : 'other');
114
+ || 'other';
128
115
 
129
116
  let label = (overlayEntry && overlayEntry.label)
130
117
  || (classified && classified.label)
@@ -149,13 +136,13 @@ function buildCatalog(swagger, sampleDocs) {
149
136
  tsvHeader,
150
137
  group,
151
138
  order: overlayEntry && overlayEntry.order != null ? overlayEntry.order : null,
152
- description: (overlayEntry && overlayEntry.description) || (swaggerEntry && swaggerEntry.description) || '',
153
- type: sampleType || (swaggerEntry && swaggerEntry.type) || 'unknown',
139
+ description: (overlayEntry && overlayEntry.description) || '',
140
+ type: sampleType || 'unknown',
154
141
  multiValued,
155
142
  patternId: classified ? classified.patternId : null,
156
143
  matchGroups: classified ? classified.matchGroups : null,
157
144
  expression: !!(classified && classified.expression),
158
- source: overlayEntry ? 'overlay' : (swaggerEntry ? 'swagger' : 'sample')
145
+ source: overlayEntry ? 'overlay' : 'sample'
159
146
  };
160
147
  }
161
148
 
@@ -181,7 +168,6 @@ function buildCatalog(swagger, sampleDocs) {
181
168
 
182
169
  return {
183
170
  fetchedAt: Date.now(),
184
- swaggerInfo: swagger && swagger.info ? swagger.info : null,
185
171
  groups,
186
172
  fields
187
173
  };
@@ -194,59 +180,14 @@ const grameneFieldCatalog = createAsyncResourceBundle({
194
180
  staleAfter: 5 * 60 * 1000,
195
181
  getPromise: ({ store }) => {
196
182
  const api = store.selectGrameneAPI();
197
- const swaggerUrl = typeof store.selectGrameneSwaggerURL === 'function'
198
- ? store.selectGrameneSwaggerURL()
199
- : `${api}/swagger`;
200
- const sampleUrl = `${api}/search?q=${encodeURIComponent(SAMPLE_QUERY)}&rows=${SAMPLE_ROWS}&fl=*`;
201
- const experimentsUrl = `${api}/experiments?rows=-1`;
202
- const mapsUrl = `${api}/maps?rows=-1`;
203
- return Promise.all([
204
- fetch(swaggerUrl).then(r => r.json()).catch(() => null),
205
- fetch(sampleUrl).then(r => r.json()).catch(() => null),
206
- fetch(experimentsUrl).then(r => r.json()).catch(() => []),
207
- fetch(mapsUrl).then(r => r.json()).catch(() => [])
208
- ]).then(([swagger, sample, experiments, maps]) => {
209
- const docs = (sample && sample.response && sample.response.docs) || [];
210
- // Determine which species (anchor taxa) have differential experiments,
211
- // then for each run a narrow discovery query scoped to that species'
212
- // strain taxa so we actually surface their diffexpr field names.
213
- const diffTaxa = [...new Set(
214
- (experiments || [])
215
- .filter(e => e && e.type === 'Differential' && e.taxon_id)
216
- .map(e => e.taxon_id)
217
- )];
218
- const strainsByAnchor = {};
219
- for (const m of (maps || [])) {
220
- const anchor = m && m.anchor_taxon_id;
221
- if (!anchor) continue;
222
- (strainsByAnchor[anchor] = strainsByAnchor[anchor] || []).push(m.taxon_id);
223
- }
224
- const discoveryFetches = diffTaxa.map(t => {
225
- const strains = strainsByAnchor[t] || [t];
226
- const fq = `taxon_id:(${strains.join(' OR ')})`;
227
- const url = `${api}/search?q=${encodeURIComponent(SAMPLE_QUERY)}`
228
- + `&rows=${DIFFEXPR_ROWS_PER_TAXON}`
229
- + `&fl=${encodeURIComponent(DIFFEXPR_FL)}`
230
- + `&fq=${encodeURIComponent(fq)}`;
231
- return fetch(url).then(r => r.json()).catch(() => null);
232
- });
233
- return Promise.all(discoveryFetches).then(results => {
234
- const diffDocs = results.flatMap(r => (r && r.response && r.response.docs) || []);
235
- return buildCatalog(swagger, [...docs, ...diffDocs]);
236
- });
237
- });
183
+ const url = `${api}/search?q=*:*&rows=0&wt=csv&fl=*`;
184
+ return fetch(url)
185
+ .then(r => r.text())
186
+ .then(text => parseCsvHeader(text))
187
+ .catch(() => null);
238
188
  }
239
189
  });
240
190
 
241
- grameneFieldCatalog.reactGrameneFieldCatalog = createSelector(
242
- 'selectGrameneFieldCatalogShouldUpdate',
243
- (shouldUpdate) => {
244
- if (shouldUpdate) {
245
- return { actionCreator: 'doFetchGrameneFieldCatalog' };
246
- }
247
- }
248
- );
249
-
250
191
  function assayFactorLabel(assay) {
251
192
  if (!assay) return '';
252
193
  const factors = Array.isArray(assay.factor) ? assay.factor : [];
@@ -592,13 +533,51 @@ function enrichLabels(catalog, expressionStudies, expressionSamples, grameneSear
592
533
  return { ...catalog, fields: fieldsOut, groups };
593
534
  }
594
535
 
536
+ function filterCatalogByPresentFields(catalog, present) {
537
+ if (!catalog || !catalog.fields || !present) return catalog;
538
+ const filteredFields = {};
539
+ for (const [name, f] of Object.entries(catalog.fields)) {
540
+ if (present.has(name)) filteredFields[name] = f;
541
+ }
542
+ const hasContent = (g) =>
543
+ (g.fields && g.fields.length > 0) ||
544
+ (g.subgroups && g.subgroups.some(hasContent));
545
+ const filterGroup = (g) => {
546
+ const fields = (g.fields || []).filter(n => present.has(n));
547
+ const subgroups = (g.subgroups || [])
548
+ .map(filterGroup)
549
+ .filter(hasContent);
550
+ return { ...g, fields, subgroups };
551
+ };
552
+ const groups = (catalog.groups || []).map(filterGroup).filter(hasContent);
553
+ return { ...catalog, fields: filteredFields, groups };
554
+ }
555
+
595
556
  grameneFieldCatalog.selectFieldCatalog = createSelector(
596
557
  'selectGrameneFieldCatalog',
597
558
  'selectExpressionStudies',
598
559
  'selectExpressionSamples',
599
560
  'selectGrameneSearch',
600
561
  'selectGrameneMaps',
601
- enrichLabels
562
+ (fieldNames, studies, samples, search, maps) => {
563
+ if (!fieldNames || !fieldNames.length) return null;
564
+ const present = new Set(fieldNames);
565
+ // Build a synthetic doc from the discovered field names; values carry the
566
+ // right JS type so inferType picks the correct multiValued/type (arrays
567
+ // for multi-valued fields, scalars otherwise). Derive pval/logfc
568
+ // companions from every l2fc field.
569
+ const doc = {};
570
+ for (const name of present) {
571
+ doc[name] = synthesizedValue(name);
572
+ if (/_l2fc_attr_f$/.test(name)) {
573
+ doc[name.replace('_l2fc_', '_pval_')] = 0;
574
+ doc[name.replace('_l2fc_', '_logfc_')] = 0;
575
+ }
576
+ }
577
+ const catalog = buildCatalog([doc]);
578
+ const enriched = enrichLabels(catalog, studies, samples, search, maps);
579
+ return filterCatalogByPresentFields(enriched, present);
580
+ }
602
581
  );
603
582
 
604
583
  grameneFieldCatalog.selectFieldCatalogGroups = createSelector(
@@ -115,7 +115,7 @@ const grameneViews = {
115
115
  const touched = raw.touched || {};
116
116
  const numFound = (search && search.response && search.response.numFound) || 0;
117
117
  const hasFilters = !!(filters && filters.rightIdx > 1);
118
- const resultDependentIds = new Set(['taxonomy', 'list']);
118
+ const resultDependentIds = new Set(['taxonomy', 'list', 'export']);
119
119
  const autoDisable = (numFound === 0) || !hasFilters;
120
120
  const hasFirebase = !!(config && config.firebaseConfig);
121
121
 
@@ -127,8 +127,8 @@ const grameneViews = {
127
127
  options = options.filter(v => overrides[v.id] !== 'hidden');
128
128
  }
129
129
  options = options.map(v => {
130
- if (autoDisable && resultDependentIds.has(v.id)) {
131
- return { ...v, show: 'disabled' };
130
+ if (resultDependentIds.has(v.id) && autoDisable) {
131
+ return { ...v, show: 'off' };
132
132
  }
133
133
  if (!overrides) return v;
134
134
  const o = overrides[v.id];
@@ -137,6 +137,10 @@ const grameneViews = {
137
137
  if (touched[v.id]) return v;
138
138
  return { ...v, show: o };
139
139
  });
140
+ const anyOtherOn = options.some(v => v.id !== 'help' && v.show === 'on');
141
+ if (!anyOtherOn) {
142
+ options = options.map(v => v.id === 'help' ? { ...v, show: 'on' } : v);
143
+ }
140
144
  return { ...raw, options };
141
145
  }
142
146
  )
@@ -12,13 +12,19 @@ const ExporterViewCmp = props => {
12
12
  const {
13
13
  fieldCatalog: catalog,
14
14
  grameneFieldCatalogIsLoading: isLoading,
15
- grameneFieldCatalogRaw: raw
15
+ grameneFieldCatalogRaw: raw,
16
+ grameneFieldCatalogShouldUpdate: shouldUpdate,
17
+ doFetchGrameneFieldCatalog
16
18
  } = props;
17
19
  const [fieldQuery, setFieldQuery] = useState('');
18
20
  const [leftPct, setLeftPct] = useState(50);
19
21
  const [dragging, setDragging] = useState(false);
20
22
  const containerRef = useRef(null);
21
23
 
24
+ useEffect(() => {
25
+ if (shouldUpdate) doFetchGrameneFieldCatalog();
26
+ }, [shouldUpdate, doFetchGrameneFieldCatalog]);
27
+
22
28
  const onPointerMove = useCallback((e) => {
23
29
  const el = containerRef.current;
24
30
  if (!el) return;
@@ -103,5 +109,7 @@ export default connect(
103
109
  'selectFieldCatalog',
104
110
  'selectGrameneFieldCatalogIsLoading',
105
111
  'selectGrameneFieldCatalogRaw',
112
+ 'selectGrameneFieldCatalogShouldUpdate',
113
+ 'doFetchGrameneFieldCatalog',
106
114
  ExporterViewCmp
107
115
  );
@@ -170,5 +170,6 @@
170
170
  "compara_idx", "compara_idx_multi",
171
171
  "_id", "annotations", "bins", "gene_structure",
172
172
  "homology", "location", "xrefs",
173
- "domain_roots", "familyRoot__ancestors", "taxonomy__ancestors", "capabilities"
173
+ "domain_roots", "familyRoot__ancestors", "taxonomy__ancestors", "capabilities",
174
+ "saved_search"
174
175
  ]}