UNPKG

30.5 kBPlain TextView Raw
1/// <reference path="../typings/index.d.ts" />
2
3// TODO use a cache, such as
4// https://github.com/levelgraph/levelgraph
5
6import "source-map-support/register";
7
8declare global {
9 // Augment Node.js `global`
10 namespace NodeJS {
11 interface Global {
12 XMLHttpRequest: XMLHttpRequest;
13 }
14 }
15 // Augment Browser `window`
16 //interface Window extends NodeJS.Global { }
17 // Augment Web Worker `self`
18 //interface WorkerGlobalScope extends NodeJS.Global { }
19}
20
21if (!global.hasOwnProperty("XMLHttpRequest")) {
22 global.XMLHttpRequest = require("xhr2");
23}
24
25import { curry, negate, uniq } from "lodash/fp";
26import {
27 camelCase,
28 defaultsDeep,
29 fill,
30 invert,
31 isArray,
32 isEmpty,
33 isNaN,
34 isNull,
35 isString,
36 isUndefined,
37 omitBy,
38 toPairs,
39 zip
40} from "lodash";
41import { Observable } from "rxjs/Observable";
42// TODO should I need to import the interface type definition like this?
43import { AjaxRequest } from "rxjs/observable/dom/AjaxObservable";
44import "rxjs/add/observable/dom/ajax";
45import "rxjs/add/observable/empty";
46import "rxjs/add/observable/forkJoin";
47import "rxjs/add/observable/from";
48import "rxjs/add/observable/throw";
49import "rxjs/add/observable/zip";
50import "rxjs/add/operator/buffer";
51import "rxjs/add/operator/bufferWhen";
52import "rxjs/add/operator/catch";
53import "rxjs/add/operator/concatAll";
54import "rxjs/add/operator/debounceTime";
55import "rxjs/add/operator/delay";
56import "rxjs/add/operator/distinctUntilChanged";
57import "rxjs/add/operator/do";
58import "rxjs/add/operator/filter";
59import "rxjs/add/operator/find";
60import "rxjs/add/operator/mergeMap";
61import "rxjs/add/operator/map";
62import "rxjs/add/operator/multicast";
63import "rxjs/add/operator/publishReplay";
64import "rxjs/add/operator/race";
65import "rxjs/add/operator/reduce";
66import "rxjs/add/operator/skip";
67import "rxjs/add/operator/toArray";
68import "rx-extra/add/operator/throughNodeStream";
69import { Subject } from "rxjs/Subject";
70import { TSVGetter } from "./spinoffs/TSVGetter";
71import { dataTypeParsers } from "./spinoffs/dataTypeParsers";
72import { arrayify, unionLSV } from "./spinoffs/jsonld-utils";
73const VError = require("verror");
74
75const BDB = "http://vocabularies.bridgedb.org/ops#";
76const BIOPAX = "http://www.biopax.org/release/biopax-level3.owl#";
77const IDENTIFIERS = "http://identifiers.org/";
78const OWL = "http://www.w3.org/2002/07/owl#";
79const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
80
81const CSV_OPTIONS = { objectMode: true, delimiter: "\t" };
82
83// time to wait for no new calls to xrefs() before we
84// batch up all calls in the queue and send to xrefsBatch()
85const XREF_REQUEST_DEBOUNCE_TIME = 10; // ms
86const XREF_REQUEST_CHUNK_SIZE = 100;
87
88const BRIDGE_DB_REPO_CDN =
89 "https://raw.githubusercontent.com/bridgedb/BridgeDb/";
90const BRIDGE_DB_COMMIT_HASH = "465f9f944d09cefbb167eceb9c69499a764100a2";
91export const CONFIG_DEFAULT = {
92 baseIri: "https://webservice.bridgedb.org/",
93 context: [
94 BRIDGE_DB_REPO_CDN,
95 BRIDGE_DB_COMMIT_HASH,
96 "/org.bridgedb.bio/resources/org/bridgedb/bio/jsonld-context.jsonld"
97 ].join(""),
98 dataSourcesMetadataHeadersIri: [
99 BRIDGE_DB_REPO_CDN,
100 BRIDGE_DB_COMMIT_HASH,
101 "/org.bridgedb.bio/resources/org/bridgedb/bio/datasources_headers.txt"
102 ].join(""),
103 dataSourcesMetadataIri: [
104 BRIDGE_DB_REPO_CDN,
105 BRIDGE_DB_COMMIT_HASH,
106 "/org.bridgedb.bio/resources/org/bridgedb/bio/datasources.txt"
107 ].join(""),
108 http: {
109 timeout: 4 * 1000,
110 retryLimit: 2,
111 retryDelay: 3 * 1000
112 }
113};
114
115// these properties can be trusted to
116// uniquely identify a data source.
117const DATASOURCE_ID_PROPERTIES = [
118 "id",
119 "miriamUrn",
120 "conventionalName",
121 "preferredPrefix",
122 "systemCode"
123];
124
125const IRI_TO_NAME = {
126 "http://www.w3.org/1999/02/22-rdf-syntax-ns#about": "id",
127 "http://identifiers.org/idot/preferredPrefix": "preferredPrefix",
128 "http://identifiers.org/miriam.collection/": "miriamUrn"
129};
130const NAME_TO_IRI = invert(IRI_TO_NAME);
131
132/**
133 * miriamUrnToIdentifiersIri
134 *
135 * @param {string} miriamUrn
136 * @return {string} e.g., "http://identifiers.org/ncbigene/"
137 */
138function miriamUrnToIdentifiersIri(miriamUrn: string): string {
139 const preferredPrefix = miriamUrnToPreferredPrefix(miriamUrn);
140 if (preferredPrefix) {
141 return IDENTIFIERS + preferredPrefix + "/";
142 }
143}
144
145/**
146 * miriamUrnToPreferredPrefix
147 *
148 * @param {string} uri, e.g., "urn:miriam:ncbigene"
149 * @return {string} preferredPrefix from identifiers.org/Miriam, e.g., "ncbigene"
150 */
151function miriamUrnToPreferredPrefix(miriamUrn: string): string {
152 // Make sure it's actually an identifiers.org namespace,
153 // not a BridgeDb system code:
154 if (miriamUrn.indexOf("urn:miriam:") > -1) {
155 return miriamUrn.substring(11, miriamUrn.length);
156 }
157}
158
159export interface DataSourcesMetadataHeaderRow {
160 header: string;
161 description: string;
162 example_entry: string;
163 id: string;
164 name: string;
165 "http://www.w3.org/1999/02/22-rdf-syntax-ns#datatype": string;
166}
167
168export class BridgeDb {
169 config;
170 dataSourceMappings$;
171 //dataSourcesMetadataHeaderNameToIri$;
172 getTSV;
173 private xrefsRequestQueue;
174 private xrefsResponseQueue;
175 constructor(config: Partial<typeof CONFIG_DEFAULT> = CONFIG_DEFAULT) {
176 let bridgeDb = this;
177 defaultsDeep(config, CONFIG_DEFAULT);
178 bridgeDb.config = config;
179
180 var xrefsRequestQueue = (bridgeDb.xrefsRequestQueue = new Subject());
181 var debounceSignel = xrefsRequestQueue.debounceTime(
182 XREF_REQUEST_DEBOUNCE_TIME
183 );
184
185 bridgeDb.xrefsResponseQueue = xrefsRequestQueue
186 .filter(
187 ({ organism, xrefDataSource, xrefIdentifier }) =>
188 !isEmpty(organism) &&
189 !isEmpty(xrefDataSource) &&
190 !isEmpty(xrefIdentifier)
191 )
192 /* TODO should we use this? It doesn't seem to work, and we could just use caching.
193 .distinctUntilChanged(function(
194 a: { xrefDataSource; xrefIdentifier },
195 b: { xrefDataSource; xrefIdentifier }
196 ) {
197 return JSON.stringify(a) === JSON.stringify(b);
198 })
199 //*/
200 //.buffer(Observable.race(debounceSignel, xrefsRequestQueue.skip(2000)))
201 .bufferWhen(() =>
202 Observable.race(debounceSignel, xrefsRequestQueue.skip(2000))
203 )
204 .filter(x => !isEmpty(x))
205 .mergeMap(function(
206 inputs: {
207 organism: organism;
208 xrefDataSource: string;
209 xrefIdentifier: string;
210 desiredXrefDataSources?: string[];
211 }[]
212 ) {
213 const firstInput = inputs[0];
214 const organism = firstInput.organism;
215 const xrefDataSources = inputs.map(input => input.xrefDataSource);
216 const xrefIdentifiers = inputs.map(input => input.xrefIdentifier);
217 const desiredXrefDataSources = firstInput.desiredXrefDataSources;
218 return bridgeDb.xrefsBatch(
219 organism,
220 xrefDataSources,
221 xrefIdentifiers,
222 desiredXrefDataSources
223 );
224 })
225 .multicast(new Subject());
226
227 // toggle from cold to hot
228 bridgeDb.xrefsResponseQueue.connect();
229
230 const getTSV = (bridgeDb.getTSV = new TSVGetter(config.http).get);
231
232 const dataSourcesMetadataHeaders$ = getTSV(
233 config.dataSourcesMetadataHeadersIri
234 ).map(function(fields): DataSourcesMetadataHeaderRow {
235 const id = fields[4];
236 return {
237 // NOTE: the column number could be confusing, because it's one-based,
238 // so I'll just use the index instead and ignore the column number.
239 //column: parseFloat(fields[0]),
240 header: fields[1],
241 description: fields[2],
242 example_entry: fields[3],
243 id: id,
244 name: IRI_TO_NAME.hasOwnProperty(id)
245 ? IRI_TO_NAME[id]
246 : id.split(/[\/|#]/).pop(),
247 "http://www.w3.org/1999/02/22-rdf-syntax-ns#datatype": fields[5]
248 };
249 });
250
251 bridgeDb.dataSourceMappings$ = Observable.forkJoin(
252 dataSourcesMetadataHeaders$.toArray(),
253 getTSV(config.dataSourcesMetadataIri).toArray()
254 )
255 .mergeMap(function(results) {
256 var metadataByColumnIndex = results[0];
257 var rows = results[1];
258
259 return Observable.from(rows).map(function(fields) {
260 return fields.reduce(
261 function(acc, field, i) {
262 const metadata = metadataByColumnIndex[i];
263 const { id, name } = metadata;
264 // NOTE: side effects
265 if (!!id && !(id in IRI_TO_NAME)) {
266 IRI_TO_NAME[id] = name;
267 NAME_TO_IRI[name] = id;
268 }
269 acc[name] = dataTypeParsers[metadata[RDF + "datatype"]](field);
270 return acc;
271 },
272 {} as DataSource
273 );
274 });
275 })
276 .map(function(dataSource: DataSource): DataSource {
277 // remove empty properties, ie., properties with these values:
278 // ''
279 // NaN
280 // null
281 // undefined
282 // TODO what about empty plain object {} or array []
283
284 return omitBy(dataSource, function(value: any): boolean {
285 return (
286 value === "" || isNaN(value) || isNull(value) || isUndefined(value)
287 );
288 }) as DataSource;
289 })
290 .map(function(dataSource: DataSource) {
291 // Kludge to temporarily handle this issue:
292 // https://github.com/bridgedb/BridgeDb/issues/58
293 if (dataSource.id === "Sp") {
294 dataSource.id = "urn:miriam:uniprot";
295 }
296 // If the Miriam URN is unknown or unspecified, datasources.txt uses
297 // the BridgeDb system code as a placeholder value.
298 // So here we make sure "id" is actually a Miriam URN.
299 if (
300 dataSource.hasOwnProperty("id") &&
301 dataSource.id.indexOf("urn:miriam:") > -1
302 ) {
303 // switch "id" property from Miriam URN to identifiers.org IRI
304 const miriamUrn = dataSource.id;
305 dataSource.miriamUrn = miriamUrn;
306 const preferredPrefix = miriamUrnToPreferredPrefix(miriamUrn);
307 if (preferredPrefix) {
308 dataSource.preferredPrefix = preferredPrefix;
309
310 dataSource.sameAs = dataSource.sameAs || [];
311 dataSource.sameAs.push(miriamUrn);
312
313 const identifiersIri = miriamUrnToIdentifiersIri(miriamUrn);
314 if (identifiersIri) {
315 dataSource.id = dataSource.hasIdentifiersOrgPattern = identifiersIri;
316 }
317 }
318 } else {
319 delete dataSource.id;
320 }
321 return dataSource;
322 })
323 .map(function(dataSource: DataSource) {
324 const primaryUriPattern = dataSource.hasPrimaryUriPattern;
325 if (!!primaryUriPattern) {
326 const regexXrefIdentifierPattern = dataSource.hasRegexPattern || ".*";
327
328 dataSource.hasRegexUriPattern = primaryUriPattern.replace(
329 "$id",
330 // removing ^ (start) and $ (end) from regexXrefIdentifierPattern
331 "(" + regexXrefIdentifierPattern.replace(/(^\^|\$$)/g, "") + ")"
332 );
333
334 // if '$id' is at the end of the primaryUriPattern
335 var indexOfDollaridWhenAtEnd = primaryUriPattern.length - 3;
336 if (primaryUriPattern.indexOf("$id") === indexOfDollaridWhenAtEnd) {
337 dataSource.sameAs = dataSource.sameAs || [];
338 dataSource.sameAs.push(
339 primaryUriPattern.substr(0, indexOfDollaridWhenAtEnd)
340 );
341 }
342 }
343
344 if (dataSource.type) {
345 dataSource[BDB + "type"] = dataSource.type;
346 }
347 dataSource.type = "Dataset";
348
349 return dataSource;
350 })
351 .map(function(dataSource) {
352 const bdbType = dataSource[BDB + "type"];
353 if (!!bdbType) {
354 dataSource.subject = [];
355 /* Example of using 'subject' (from the VOID docs <http://www.w3.org/TR/void/#subject>):
356 :Bio2RDF a void:Dataset;
357 dcterms:subject <http://purl.uniprot.org/core/Gene>;
358 .
359
360 The closest concepts from the WP, BioPAX and MESH vocabularies are included below,
361 with the default vocabulary being WP.
362
363 Note that in BioPAX, 'ProteinReference' is to 'Protein' as
364 'Class' is to 'Instance' or
365 'platonic ideal of http://identifiers.org/uniprot/P78527' is to
366 'one specific example of http://identifiers.org/uniprot/P78527'
367 with the same logic applying for Dna, Rna and SmallMolecule. As such, it appears the
368 subject of Uniprot is best described in BioPAX terms as biopax:ProteinReference instead
369 of biopax:Protein.
370
371 It is unclear whether the subject of Entrez Gene is biopax:DnaReference or biopax:Gene,
372 but I'm going with biopax:DnaReference for now because it appears to be analogous to
373 ProteinReference and SmallMoleculeReference.
374 //*/
375 if (
376 bdbType === "gene" ||
377 // TODO should the following two conditions be removed?
378 bdbType === "probe" ||
379 dataSource.preferredPrefix === "go"
380 ) {
381 dataSource.subject.push("GeneProduct");
382 dataSource.subject.push(BIOPAX + "DnaReference");
383 } else if (bdbType === "rna") {
384 dataSource.subject.push("Rna");
385 dataSource.subject.push(BIOPAX + "RnaReference");
386 } else if (bdbType === "protein") {
387 dataSource.subject.push("Protein");
388 dataSource.subject.push(BIOPAX + "ProteinReference");
389 } else if (bdbType === "metabolite") {
390 dataSource.subject.push("Metabolite");
391 dataSource.subject.push(BIOPAX + "SmallMoleculeReference");
392 } else if (bdbType === "pathway") {
393 // BioPAX does not have a term for pathways that is analogous to
394 // biopax:ProteinReference for proteins.
395 dataSource.subject.push("Pathway");
396 dataSource.subject.push(BIOPAX + "Pathway");
397 } else if (bdbType === "ontology") {
398 dataSource.subject.push(OWL + "Ontology");
399 } else if (bdbType === "interaction") {
400 dataSource.subject.push("Interaction");
401 dataSource.subject.push(BIOPAX + "Interaction");
402 }
403 }
404
405 dataSource.alternatePrefix = [dataSource.systemCode];
406
407 return dataSource;
408 })
409 .reduce(function(acc, dataSource) {
410 DATASOURCE_ID_PROPERTIES.forEach(function(propertyName) {
411 const propertyValue = dataSource[propertyName];
412 const propertyId = NAME_TO_IRI[propertyName];
413 dataSource[propertyId] = propertyValue;
414 acc[propertyValue] = dataSource;
415 });
416 return acc;
417 }, {})
418 .catch(err => {
419 throw new VError(err, "Setting up dataSourceMappings$ in constructor");
420 })
421 .publishReplay();
422
423 // toggle from cold to hot
424 bridgeDb.dataSourceMappings$.connect();
425 } // end constructor
426
427 attributes(
428 organism: organism,
429 xrefDataSource: string,
430 xrefIdentifier: string
431 ) {
432 let bridgeDb = this;
433 return bridgeDb
434 .getTSV(
435 bridgeDb.config.baseIri +
436 organism +
437 "/attributes/" +
438 xrefDataSource +
439 "/" +
440 xrefIdentifier
441 )
442 .reduce(function(acc, fields) {
443 const key = camelCase(fields[0]);
444 const value = fields[1];
445 acc[key] = value;
446 return acc;
447 }, {})
448 .catch(err => {
449 throw new VError(err, "calling bridgedb.attributes");
450 });
451 }
452
453 attributeSearch(
454 organism: organism,
455 query: string,
456 attrName?: string
457 ): Observable<Xref> {
458 let bridgeDb = this;
459 const attrNameParamSection = attrName ? "?attrName=" + attrName : "";
460 return bridgeDb
461 .getTSV(
462 bridgeDb.config.baseIri +
463 organism +
464 "/attributeSearch/" +
465 query +
466 attrNameParamSection
467 )
468 .mergeMap(bridgeDb.parseXrefRow)
469 .catch(err => {
470 throw new VError(err, "calling bridgedb.attributeSearch");
471 });
472 }
473
474 attributeSet(organism: organism): Observable<string[]> {
475 let bridgeDb = this;
476 return bridgeDb
477 .getTSV(bridgeDb.config.baseIri + organism + "/attributeSet")
478 .reduce(function(acc, row) {
479 acc.push(row[0]);
480 return acc;
481 }, [])
482 .catch(err => {
483 throw new VError(err, "calling bridgedb.attributeSet");
484 });
485 }
486
487 convertXrefDataSourceTo: Function = curry(
488 (targetType: string, input: string): Observable<string> => {
489 let bridgeDb = this;
490 return bridgeDb.dataSourceMappings$
491 .map(function(mapping) {
492 return !!mapping[input] && mapping[input][targetType];
493 })
494 .catch(err => {
495 throw new VError(err, "calling bridgedb.convertXrefDataSourceTo");
496 });
497 }
498 );
499
500 identifyHeaderNameForXrefDataSource = (input: string): Observable<string> => {
501 let bridgeDb = this;
502 return bridgeDb.dataSourceMappings$
503 .map(mapping => mapping[input])
504 .filter(negate(isEmpty))
505 .map(dataSource => {
506 return toPairs(dataSource)
507 .filter(([key, value]) => value === input)
508 .map(([key, value]) => key)
509 .reduce(function(acc: string, key: string): string {
510 // we want to return the IRI, if it's available.
511 return acc.length > key.length ? acc : key;
512 });
513 })
514 .catch(err => {
515 throw new VError(
516 err,
517 "calling bridgedb.identifyHeaderNameForXrefDataSource"
518 );
519 });
520 };
521
522 dataSourceProperties = (input: string): Observable<DataSource> => {
523 let bridgeDb = this;
524 return bridgeDb.dataSourceMappings$
525 .map(mapping => mapping[input])
526 .catch(err => {
527 throw new VError(err, "calling bridgedb.dataSourceProperties");
528 });
529 };
530
531 isFreeSearchSupported(organism: organism): Observable<boolean> {
532 let bridgeDb = this;
533
534 const ajaxRequest: AjaxRequest = {
535 url: bridgeDb.config.baseIri + organism + "/isFreeSearchSupported",
536 method: "GET",
537 responseType: "text",
538 timeout: bridgeDb.config.http.timeout,
539 crossDomain: true
540 };
541 return (
542 Observable.ajax(ajaxRequest)
543 .map((ajaxResponse): string => ajaxResponse.xhr.response)
544 // NOTE: must compare with 'true' as a string, because the response is just a string, not a parsed JS boolean.
545 .map(res => res === "true")
546 // TODO is this TS correct?
547 .catch(
548 (err): Observable<any> => {
549 throw new VError(err, "calling bridgedb.isFreeSearchSupported");
550 }
551 )
552 );
553 }
554
555 isMappingSupported(
556 organism: organism,
557 sourceXrefDataSource: string,
558 targetXrefDataSource: string
559 ): Observable<boolean> {
560 let bridgeDb = this;
561
562 const ajaxRequest: AjaxRequest = {
563 url: `${bridgeDb.config.baseIri +
564 organism}/isMappingSupported/${sourceXrefDataSource}/${targetXrefDataSource}`,
565 method: "GET",
566 responseType: "text",
567 timeout: bridgeDb.config.http.timeout,
568 crossDomain: true
569 };
570 return (
571 Observable.ajax(ajaxRequest)
572 .map((ajaxResponse): string => ajaxResponse.xhr.response)
573 // NOTE: must compare with 'true' as a string, because the response is just a string, not a parsed JS boolean.
574 .map(res => res === "true")
575 // TODO is this TS correct?
576 .catch(
577 (err): Observable<any> => {
578 throw new VError(err, "calling bridgedb.isMappingSupported");
579 }
580 )
581 );
582 }
583
584 organismProperties(organism: organism): Observable<{}> {
585 let bridgeDb = this;
586 return bridgeDb
587 .getTSV(bridgeDb.config.baseIri + organism + "/properties")
588 .reduce(function(acc, fields) {
589 const key = camelCase(fields[0]);
590 const value = fields[1];
591 acc[key] = value;
592 return acc;
593 }, {})
594 .catch(err => {
595 throw new VError(err, "calling bridgedb.organismProperties");
596 });
597 }
598
599 organisms(): Observable<{}> {
600 let bridgeDb = this;
601 return bridgeDb
602 .getTSV(bridgeDb.config.baseIri + "contents")
603 .map(function(fields) {
604 return {
605 en: fields[0],
606 la: fields[1]
607 };
608 })
609 .catch(err => {
610 throw new VError(err, "calling bridgedb.organisms");
611 });
612 }
613
614 private parseXrefRow = ([
615 xrefIdentifier,
616 dataSourceConventionalName,
617 symbol
618 ]: [string, string, string | undefined]): Observable<Xref> => {
619 let bridgeDb = this;
620 if (!xrefIdentifier || !dataSourceConventionalName) {
621 return Observable.empty();
622 }
623
624 return bridgeDb.dataSourceMappings$
625 .map(mapping => mapping[dataSourceConventionalName])
626 .map(function(dataSource: DataSource) {
627 let xref: Xref = {
628 xrefIdentifier: xrefIdentifier,
629 isDataItemIn: dataSource
630 };
631
632 if (symbol) {
633 xref.symbol = symbol;
634 }
635
636 if (dataSource.hasOwnProperty("id")) {
637 xref.id = encodeURI(dataSource.id + xref.xrefIdentifier);
638 }
639
640 return xref;
641 });
642 };
643
644 search(organism: organism, query: string): Observable<Xref> {
645 let bridgeDb = this;
646 return bridgeDb
647 .getTSV(bridgeDb.config.baseIri + organism + "/search/" + query)
648 .mergeMap(bridgeDb.parseXrefRow)
649 .catch(err => {
650 throw new VError(err, "calling bridgedb.search");
651 });
652 }
653
654 sourceDataSources(organism: organism): Observable<DataSource> {
655 let bridgeDb = this;
656 return bridgeDb
657 .getTSV(bridgeDb.config.baseIri + organism + "/sourceDataSources")
658 .map(function(fields) {
659 return fields[0];
660 })
661 .mergeMap(bridgeDb.dataSourceProperties)
662 .catch(err => {
663 throw new VError(err, "calling bridgedb.sourceDataSources");
664 });
665 }
666
667 targetDataSources(organism: organism): Observable<DataSource> {
668 let bridgeDb = this;
669 return bridgeDb
670 .getTSV(bridgeDb.config.baseIri + organism + "/targetDataSources")
671 .map(function(fields) {
672 return fields[0];
673 })
674 .mergeMap(bridgeDb.dataSourceProperties)
675 .catch(err => {
676 throw new VError(err, "calling bridgedb.targetDataSources");
677 });
678 }
679
680 // TODO check whether dataSource exists before calling webservice re:
681 // dataSource AND identifier
682 xrefExists(
683 organism: organism,
684 xrefDataSource: string,
685 xrefIdentifier: string
686 ): Observable<boolean> {
687 let bridgeDb = this;
688
689 const ajaxRequest: AjaxRequest = {
690 url: `${bridgeDb.config.baseIri +
691 organism}/xrefExists/${xrefDataSource}/${xrefIdentifier}`,
692 method: "GET",
693 responseType: "text",
694 timeout: bridgeDb.config.http.timeout,
695 crossDomain: true
696 };
697 return (
698 Observable.ajax(ajaxRequest)
699 .map((ajaxResponse): string => ajaxResponse.xhr.response)
700 // NOTE: must compare with 'true' as a string, because the response is just a string, not a parsed JS boolean.
701 .map(res => res === "true")
702 // TODO is this TS correct?
703 .catch(
704 (err): Observable<any> => {
705 throw new VError(err, "calling bridgedb.xrefExists");
706 }
707 )
708 );
709 }
710
711 xrefs(
712 organism: organism,
713 xrefDataSource: string,
714 xrefIdentifier: string,
715 desiredXrefDataSourceOrSources?: string
716 ): Observable<Xref> {
717 let bridgeDb = this;
718 let xrefsRequestQueue = bridgeDb.xrefsRequestQueue;
719 let xrefsResponseQueue = bridgeDb.xrefsResponseQueue;
720 const desiredXrefDataSources = arrayify(desiredXrefDataSourceOrSources);
721
722 xrefsRequestQueue.next({
723 organism,
724 xrefDataSource,
725 xrefIdentifier,
726 desiredXrefDataSources
727 });
728
729 return (
730 xrefsResponseQueue
731 .find(function(xrefBatchEnvelope) {
732 return (
733 xrefBatchEnvelope.organism === organism &&
734 // NOTE: we are not using the dataSource test in the line below.
735 // Instead, we are matching dataSources in the mergeMap further below.
736 // The reason is that the inputXrefDataSource and the returned dataSource
737 // may not match, e.g., 'L' vs. 'Entrez Gene'.
738 xrefBatchEnvelope.inputXrefDataSource === xrefDataSource &&
739 xrefBatchEnvelope.inputXrefIdentifier === xrefIdentifier &&
740 xrefBatchEnvelope.desiredXrefDataSources.join() ===
741 desiredXrefDataSources.join()
742 );
743 })
744 .map(x => x.xrefs)
745 //.do(null, xrefsRequestQueue.complete)
746 .catch(err => {
747 throw new VError(err, "calling bridgedb.xrefs");
748 })
749 );
750 }
751
752 xrefsBatch = (
753 organism: organism,
754 oneOrMoreXrefDataSources: string | string[],
755 xrefIdentifiers: string[],
756 desiredXrefDataSourceOrSources?: string | string[]
757 ): Observable<{
758 organism: string;
759 inputXrefIdentifier: string;
760 inputXrefDataSource: string;
761 xrefs: Xref[];
762 }> => {
763 let bridgeDb = this;
764 const desiredXrefDataSources = arrayify(
765 desiredXrefDataSourceOrSources
766 ) as string[];
767 const dataSourceFilterParamSection =
768 desiredXrefDataSources.length === 1
769 ? "?dataSource=" + desiredXrefDataSources[0]
770 : "";
771
772 const xrefDataSources = isArray(oneOrMoreXrefDataSources)
773 ? oneOrMoreXrefDataSources
774 : fill(new Array(xrefIdentifiers.length), oneOrMoreXrefDataSources);
775
776 const convertXrefDataSourceToConventionalName = bridgeDb.convertXrefDataSourceTo(
777 "conventionalName"
778 );
779
780 const callString = `Called xrefsBatch(
781 ${organism},
782 ${oneOrMoreXrefDataSources},
783 ${xrefIdentifiers},
784 ${desiredXrefDataSourceOrSources}
785)`;
786
787 const postURL =
788 bridgeDb.config.baseIri +
789 organism +
790 "/xrefsBatch" +
791 dataSourceFilterParamSection;
792
793 const inputXrefDataSourceHeaderName$ = Observable.from(xrefDataSources)
794 .mergeMap(function(xrefDataSource) {
795 return bridgeDb.identifyHeaderNameForXrefDataSource(xrefDataSource);
796 })
797 .find(isString);
798
799 const desiredXrefDataSourceHeaderName$ = isEmpty(desiredXrefDataSources)
800 ? inputXrefDataSourceHeaderName$
801 : Observable.from(desiredXrefDataSources)
802 .mergeMap(function(xrefDataSource) {
803 return bridgeDb.identifyHeaderNameForXrefDataSource(xrefDataSource);
804 })
805 .find(isString);
806
807 const dataSourceConventionalNames$ = Observable.from(xrefDataSources)
808 .mergeMap(function(xrefDataSource) {
809 return convertXrefDataSourceToConventionalName(xrefDataSource);
810 })
811 .toArray();
812
813 return Observable.forkJoin(
814 inputXrefDataSourceHeaderName$,
815 desiredXrefDataSourceHeaderName$,
816 dataSourceConventionalNames$
817 ).mergeMap(function([
818 inputXrefDataSourceHeaderName,
819 desiredXrefDataSourceHeaderName,
820 dataSourceConventionalNames
821 ]) {
822 // TODO: find out how we're getting duplicate rows in the body.
823 // For at least one example, see RefSeqSample.tsv in test dir.
824 // It has duplicates.
825 const body = uniq(
826 zip(xrefIdentifiers, dataSourceConventionalNames)
827 .filter(pair => !!pair[1])
828 .map(x => x.join("\t"))
829 ).join("\n");
830
831 if (isEmpty(body.replace(/[\ \n\t]/g, ""))) {
832 return Observable.throw(
833 new Error(`Error: body is empty. ${callString}`)
834 );
835 }
836
837 const convertXrefDataSourceToInputFormat = bridgeDb.convertXrefDataSourceTo(
838 inputXrefDataSourceHeaderName
839 );
840 const convertXrefDataSourceToDesiredInputFormat = bridgeDb.convertXrefDataSourceTo(
841 desiredXrefDataSourceHeaderName
842 );
843
844 return bridgeDb
845 .getTSV(postURL, "POST", body)
846 .mergeMap(function(xrefStringsByInput) {
847 const inputXrefIdentifier = xrefStringsByInput[0];
848 const inputXrefDataSource = xrefStringsByInput[1];
849 const xrefsString = xrefStringsByInput[2];
850
851 // NOTE: splitting by comma, e.g.:
852 // 'T:GO:0031966,Il:ILMN_1240829' -> ['T:GO:0031966', 'Il:ILMN_1240829']
853 return Observable.from(xrefsString.split(","))
854 .mergeMap(function(
855 xrefString: string
856 ): Observable<Record<"xrefDataSource" | "xrefIdentifier", string>> {
857 if (xrefString === "N/A") {
858 return Observable.empty();
859 }
860
861 // NOTE: splitting by FIRST colon only, e.g.:
862 // 'T:GO:0031966' -> ['T', 'GO:0031966']
863 const [
864 returnedXrefDataSource,
865 returnedXrefIdentifier
866 ] = xrefString.split(/:(.+)/);
867
868 return convertXrefDataSourceToDesiredInputFormat(
869 returnedXrefDataSource
870 ).map(function(desiredXrefDataSource) {
871 return {
872 xrefDataSource: desiredXrefDataSource,
873 xrefIdentifier: returnedXrefIdentifier
874 };
875 });
876 })
877 .filter(({ xrefDataSource }) => {
878 return (
879 !isEmpty(xrefDataSource) &&
880 (desiredXrefDataSources.length === 0 ||
881 desiredXrefDataSources.indexOf(xrefDataSource) > -1)
882 );
883 })
884 .toArray()
885 .mergeMap(function(xrefs) {
886 if (desiredXrefDataSources.length > 0) {
887 // Sort xrefs in the order matching the order that the user specified
888 // in desiredXrefDataSource1, desiredXrefDataSource2, ...
889 xrefs.sort(function(a, b) {
890 const aIndex = desiredXrefDataSources.indexOf(
891 a.xrefDataSource
892 );
893 const bIndex = desiredXrefDataSources.indexOf(
894 b.xrefDataSource
895 );
896 if (aIndex < bIndex) {
897 return -1;
898 } else if (aIndex > bIndex) {
899 return 1;
900 } else {
901 return 0;
902 }
903 });
904 }
905
906 return convertXrefDataSourceToInputFormat(
907 inputXrefDataSource
908 ).map(function(inputXrefDataSource) {
909 return {
910 organism,
911 inputXrefDataSource,
912 inputXrefIdentifier,
913 xrefs,
914 // NOTE: return desiredXrefDataSources for use in xrefsResponseQueue
915 desiredXrefDataSources
916 };
917 });
918 });
919 })
920 .catch(null, function(err) {
921 throw new VError(err, `Error: ${callString}`);
922 });
923 });
924 };
925}