Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Endpoint for retrieving hierarchy information for multiple TSNs #250

Merged
merged 12 commits into from
Aug 28, 2024
106 changes: 106 additions & 0 deletions api/src/paths/taxonomy/taxon/tsn/hierarchy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { getAPIUserDBConnection } from '../../../../database/db';
import { ItisService } from '../../../../services/itis-service';
import { getLogger } from '../../../../utils/logger';

const defaultLog = getLogger('paths/taxonomy/taxon/tsn/hierarchy');

export const GET: Operation = [getTaxonHierarchyByTSN()];

GET.apiDoc = {
description: 'Get taxon records by TSN ids.',
tags: ['taxon_id'],
security: [
{
Bearer: []
}
mauberti-bc marked this conversation as resolved.
Show resolved Hide resolved
],
parameters: [
{
description: 'Taxon TSN ids.',
in: 'query',
name: 'tsn',
schema: {
type: 'array',
description: 'One or more Taxon TSN ids.',
items: {
type: 'integer',
minimum: 0,
minItems: 1,
maxItems: 100
}
},
required: true
}
],
responses: {
200: {
description: 'Taxonomy response.',
content: {
'application/json': {
schema: {
type: 'array',
items: {
title: 'Species',
type: 'object',
required: ['tsn', 'hierarchy'],
properties: {
tsn: {
type: 'integer'
},
hierarchy: { type: 'array', items: { type: 'integer' } }
mauberti-bc marked this conversation as resolved.
Show resolved Hide resolved
},
additionalProperties: false
}
}
}
}
},
400: {
$ref: '#/components/responses/400'
},
401: {
$ref: '#/components/responses/401'
},
500: {
$ref: '#/components/responses/500'
},
default: {
$ref: '#/components/responses/default'
}
}
};

/**
* Get taxon by ITIS TSN.
*
* @returns {RequestHandler}
*/
export function getTaxonHierarchyByTSN(): RequestHandler {
return async (req, res) => {
defaultLog.debug({ label: 'getTaxonHierarchyByTSN', message: 'query params', query: req.query });

const connection = getAPIUserDBConnection();

const tsnIds: number[] = (req.query.tsn as (string | number)[]).map(Number);
mauberti-bc marked this conversation as resolved.
Show resolved Hide resolved

try {
await connection.open();

const itisService = new ItisService();

const response = await itisService.getHierarchyForTSNs(tsnIds);

connection.commit();

res.status(200).json(response);
} catch (error) {
defaultLog.error({ label: 'getTaxonHierarchyByTSN', message: 'error', error });
connection.rollback();
throw error;
} finally {
connection.release();
}
};
}
80 changes: 80 additions & 0 deletions api/src/services/itis-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ export type ItisSolrSearchResponse = {
rank: string;
};

export type ItisSolrSearchResponseHierarchy = {
tsn: string;
hierarchyTSN: [string]; // Array with one item
};

export type TSNWithHierarchy = {
tsn: number;
hierarchy: number[];
};

/**
* Service for retrieving and processing taxonomic data from the Integrated Taxonomic Information System (ITIS).
*
Expand Down Expand Up @@ -75,10 +85,32 @@ export class ItisService {
return response.data.response.docs;
}

/**
* Returns the parent hierarchy for multiple TSNs
*
* @param {number[]} tsnIds
* @return {*} {Promise<TSNWithHierarchy[]>}
* @memberof ItisService
*/
async getHierarchyForTSNs(tsnIds: number[]): Promise<TSNWithHierarchy[]> {
const url = await this.getItisSolrTsnHierarchyUrl(tsnIds);

defaultLog.debug({ label: 'getHierarchyForTSNs', message: 'url', url });

const response = await axios.get(url);
mauberti-bc marked this conversation as resolved.
Show resolved Hide resolved

if (!response.data || !response.data.response || !response.data.response.docs) {
return [];
}

return this._sanitizeHierarchyData(response.data.response.docs);
}

/**
* Cleans up the ITIS search response data
*
* @param {ItisSolrSearchResponse[]} data
* @return {*} {Promise<TaxonSearchResult[]>}
* @memberof ItisService
*/
_sanitizeItisData = (data: ItisSolrSearchResponse[]): TaxonSearchResult[] => {
Expand All @@ -95,6 +127,22 @@ export class ItisService {
});
};

/**
* Cleans up the ITIS hierarchy response data
*
* @param {ItisSolrSearchResponse[]} data
* @memberof ItisService
*/
_sanitizeHierarchyData = (data: ItisSolrSearchResponseHierarchy[]): TSNWithHierarchy[] => {
return data.map((item: ItisSolrSearchResponseHierarchy) => ({
tsn: Number(item.tsn),
hierarchy: item.hierarchyTSN[0]
.split('$')
.filter((part) => part !== '')
.map((tsn) => Number(tsn))
}));
};

/**
* Get the ITIS SORL search-by-term URL.
*
Expand Down Expand Up @@ -139,6 +187,28 @@ export class ItisService {
)}&${this._getItisSolrFilterParam()}&&q=${this._getItisSolrTsnSearch(searchTsnIds)}`;
}

/**
* Get the ITIS SOLR search-by-tsn URL for hierarchy information
*
* @param {number[]} tsnIds
* @return {*} {Promise<string>}
* @memberof ItisService
*/
async getItisSolrTsnHierarchyUrl(tsnIds: number[]): Promise<string> {
const itisUrl = this._getItisSolrUrl();

if (!itisUrl) {
defaultLog.debug({ label: 'getItisTsnHierarchyUrl', message: 'Environment variable ITIS_URL is not defined.' });
throw new Error('Failed to build ITIS query.');
}

return `${itisUrl}??${this._getItisSolrTypeParam()}&${this._getItisSolrSortParam(
['kingdom'],
['asc'],
150
)}&${this._getItisSolrHierarchyParam()}&&q=${this._getItisSolrTsnSearch(tsnIds)}`;
}

/**
* Get ITIS SOLR base URL.
*
Expand Down Expand Up @@ -182,6 +252,16 @@ export class ItisService {
return 'omitHeader=true&fl=tsn+scientificName:nameWOInd+kingdom+parentTSN+commonNames:vernacular+updateDate+usage+rank';
}

/**
* Get ITIS SOLR filter param.
*
* @return {*} {string}
* @memberof ItisService
*/
_getItisSolrHierarchyParam(): string {
return 'omitHeader=true&fl=tsn+hierarchyTSN';
}

/**
* Get ITIS SOLR query by search term param.
*
Expand Down
Loading