diff --git a/api/src/paths/taxonomy/taxon/index.ts b/api/src/paths/taxonomy/taxon/index.ts index 80ccd241..09ccd0ed 100644 --- a/api/src/paths/taxonomy/taxon/index.ts +++ b/api/src/paths/taxonomy/taxon/index.ts @@ -48,7 +48,7 @@ GET.apiDoc = { required: ['tsn', 'label'], properties: { tsn: { - type: 'number' + type: 'integer' }, label: { type: 'string' diff --git a/api/src/paths/taxonomy/taxon/tsn/index.ts b/api/src/paths/taxonomy/taxon/tsn/index.ts index 59484f6a..1aee2ea3 100644 --- a/api/src/paths/taxonomy/taxon/tsn/index.ts +++ b/api/src/paths/taxonomy/taxon/tsn/index.ts @@ -49,7 +49,7 @@ GET.apiDoc = { required: ['tsn', 'label'], properties: { tsn: { - type: 'number' + type: 'integer' }, label: { type: 'string' diff --git a/api/src/repositories/taxonomy-repository.ts b/api/src/repositories/taxonomy-repository.ts index 143e7fc3..d7ef6a3b 100644 --- a/api/src/repositories/taxonomy-repository.ts +++ b/api/src/repositories/taxonomy-repository.ts @@ -4,7 +4,7 @@ import { getKnex } from '../database/db'; import { ApiExecuteSQLError } from '../errors/api-error'; import { BaseRepository } from './base-repository'; -export const ItisTaxonRecord = z.object({ +export const TaxonRecord = z.object({ taxon_id: z.number(), itis_tsn: z.number(), bc_taxon_code: z.string().nullable(), @@ -20,7 +20,7 @@ export const ItisTaxonRecord = z.object({ revision_count: z.number() }); -export type ItisTaxonRecord = z.infer; +export type TaxonRecord = z.infer; /** * Taxonomy Repository @@ -34,13 +34,13 @@ export class TaxonomyRepository extends BaseRepository { * Get taxon records by TSN id. * * @param {number[]} tsnIds - * @return {*} {Promise} + * @return {*} {Promise} * @memberof TaxonomyRepository */ - async getTaxonByTsnIds(tsnIds: number[]): Promise { + async getTaxonByTsnIds(tsnIds: number[]): Promise { const queryBuilder = getKnex().queryBuilder().select('*').from('taxon').whereIn('itis_tsn', tsnIds); - const response = await this.connection.knex(queryBuilder, ItisTaxonRecord); + const response = await this.connection.knex(queryBuilder, TaxonRecord); return response.rows; } @@ -54,7 +54,7 @@ export class TaxonomyRepository extends BaseRepository { * @param {string} commonName * @param {Record} itisData * @param {string} itisUpdateDate - * @return {*} {Promise} + * @return {*} {Promise} * @memberof TaxonomyRepository */ async addItisTaxonRecord( @@ -63,7 +63,7 @@ export class TaxonomyRepository extends BaseRepository { commonName: string | null, itisData: Record, itisUpdateDate: string - ): Promise { + ): Promise { const sqlStatement = SQL` INSERT INTO taxon @@ -85,7 +85,7 @@ export class TaxonomyRepository extends BaseRepository { *; `; - const response = await this.connection.sql(sqlStatement, ItisTaxonRecord); + const response = await this.connection.sql(sqlStatement, TaxonRecord); if (response.rowCount !== 1) { throw new ApiExecuteSQLError('Failed to insert new taxon record', [ diff --git a/api/src/services/itis-service.ts b/api/src/services/itis-service.ts index 015431ed..5e2b764f 100644 --- a/api/src/services/itis-service.ts +++ b/api/src/services/itis-service.ts @@ -1,24 +1,10 @@ import axios from 'axios'; import { getLogger } from '../utils/logger'; +import { TaxonSearchResult } from './taxonomy-service'; const defaultLog = getLogger('services/taxonomy-service'); -export interface ITaxonomySource { - unit_name1: string; - unit_name2: string; - unit_name3: string; - taxon_authority: string; - code: string; - tty_kingdom: string; - tty_name: string; - english_name: string; - note: string | null; - end_date: string | null; - parent_id: number | null; - parent_hierarchy: { id: number; level: string }[]; -} - -export interface IItisSearchResponse { +export type ItisSolrSearchResponse = { commonNames: string[]; kingdom: string; name: string; @@ -27,13 +13,7 @@ export interface IItisSearchResponse { tsn: string; updateDate: string; usage: string; -} - -export interface IItisSearchResult { - tsn: number; - label: string; - scientificName: string; -} +}; /** * Service for retrieving and processing taxonomic data from the Integrated Taxonomic Information System (ITIS). @@ -48,10 +28,10 @@ export class ItisService { * Returns the ITIS search species Query. * * @param {*} searchTerms - * @return {*} {(Promise)} + * @return {*} {(Promise)} * @memberof TaxonomyService */ - async searchItisByTerm(searchTerms: string[]): Promise { + async searchItisByTerm(searchTerms: string[]): Promise { const url = await this.getItisSolrTermSearchUrl(searchTerms); defaultLog.debug({ label: 'searchItisByTerm', message: 'url', url }); @@ -69,10 +49,10 @@ export class ItisService { * Returns the ITIS search by TSN. * * @param {number[]} searchTsnIds - * @return {*} {(Promise)} + * @return {*} {(Promise)} * @memberof TaxonomyService */ - async searchItisByTSN(searchTsnIds: number[]): Promise { + async searchItisByTSN(searchTsnIds: number[]): Promise { const url = await this.getItisSolrTsnSearchUrl(searchTsnIds); defaultLog.debug({ label: 'searchItisByTSN', message: 'url', url }); @@ -89,11 +69,11 @@ export class ItisService { /** * Cleans up the ITIS search response data. * - * @param {IItisSearchResponse[]} data + * @param {ItisSolrSearchResponse[]} data * @memberof TaxonomyService */ - _sanitizeItisData = (data: IItisSearchResponse[]): IItisSearchResult[] => { - return data.map((item: IItisSearchResponse) => { + _sanitizeItisData = (data: ItisSolrSearchResponse[]): TaxonSearchResult[] => { + return data.map((item: ItisSolrSearchResponse) => { const commonName = (item.commonNames && item.commonNames[0].split('$')[1]) || item.scientificName; return { diff --git a/api/src/services/taxonomy-service.ts b/api/src/services/taxonomy-service.ts index 760521bf..064b33bf 100644 --- a/api/src/services/taxonomy-service.ts +++ b/api/src/services/taxonomy-service.ts @@ -1,6 +1,6 @@ import { IDBConnection } from '../database/db'; -import { ItisTaxonRecord, TaxonomyRepository } from '../repositories/taxonomy-repository'; -import { ItisService } from './itis-service'; +import { TaxonomyRepository, TaxonRecord } from '../repositories/taxonomy-repository'; +import { ItisService, ItisSolrSearchResponse } from './itis-service'; export interface ITaxonomySource { unit_name1: string; @@ -17,22 +17,11 @@ export interface ITaxonomySource { parent_hierarchy: { id: number; level: string }[]; } -export interface IItisSearchResponse { - commonNames: string[]; - kingdom: string; - name: string; - parentTSN: string; - scientificName: string; - tsn: string; - updateDate: string; - usage: string; -} - -export interface IItisSearchResult { +export type TaxonSearchResult = { tsn: number; label: string; scientificName: string; -} +}; /** * Service for retrieving and processing taxonomic data from BioHub. @@ -51,62 +40,61 @@ export class TaxonomyService { * Get taxon records by TSN ids. * * @param {number[]} tsnIds - * @return {*} {Promise} + * @return {*} {Promise} * @memberof TaxonomyService */ - async getTaxonByTsnIds(tsnIds: number[]): Promise { + async getTaxonByTsnIds(tsnIds: number[]): Promise { // Search for taxon records in the database - const taxon = await this.taxonRepository.getTaxonByTsnIds(tsnIds); + const existingTaxonRecords = await this.taxonRepository.getTaxonByTsnIds(tsnIds); - // If taxon records are found, return them - if (taxon.length > 0) { - return this._sanitizeTaxonRecordsData(taxon); - } + const missingTsnIds = tsnIds.filter((tsnId) => !existingTaxonRecords.find((item) => item.itis_tsn === tsnId)); - // If no taxon records are found, search ITIS for the taxon records - const itisService = new ItisService(); - const itisResponse = await itisService.searchItisByTSN(tsnIds); + if (missingTsnIds.length) { + // If the local database does not contain a record for all of the requested ids, search ITIS for the missing + // taxon records, patching the missing records in the local database in the process + const itisService = new ItisService(); + const itisResponse = await itisService.searchItisByTSN(missingTsnIds); - const taxonRecords: ItisTaxonRecord[] = []; - for (const itisRecord of itisResponse) { - // Add the taxon record to the database - const taxonRecord = await this.addItisTaxonRecord(itisRecord); - taxonRecords.push(taxonRecord); + for (const itisRecord of itisResponse) { + // Add the taxon record to the database + const newTaxonRecord = await this.addItisTaxonRecord(itisRecord); + existingTaxonRecords.push(newTaxonRecord); + } } - // Return the taxon records - return this._sanitizeTaxonRecordsData(taxonRecords); + // Missing ids patched, return taxon records for all requested ids + return this._sanitizeTaxonRecordsData(existingTaxonRecords); } - _sanitizeTaxonRecordsData(itisData: ItisTaxonRecord[]): IItisSearchResult[] { - return itisData.map((item: ItisTaxonRecord) => { + _sanitizeTaxonRecordsData(taxonRecords: TaxonRecord[]): TaxonSearchResult[] { + return taxonRecords.map((item: TaxonRecord) => { return { tsn: item.itis_tsn, label: item.common_name || item.itis_scientific_name, scientificName: item.itis_scientific_name - } as IItisSearchResult; + }; }); } /** * Adds a new taxon record. * - * @param {IItisSearchResponse} itisResponse - * @return {*} {Promise} + * @param {ItisSolrSearchResponse} itisSolrResponse + * @return {*} {Promise} * @memberof TaxonomyService */ - async addItisTaxonRecord(itisResponse: IItisSearchResponse): Promise { + async addItisTaxonRecord(itisSolrResponse: ItisSolrSearchResponse): Promise { let commonName = null; - if (itisResponse.commonNames) { - commonName = itisResponse.commonNames && itisResponse.commonNames[0].split('$')[1]; + if (itisSolrResponse.commonNames) { + commonName = itisSolrResponse.commonNames && itisSolrResponse.commonNames[0].split('$')[1]; } return this.taxonRepository.addItisTaxonRecord( - Number(itisResponse.tsn), - itisResponse.scientificName, + Number(itisSolrResponse.tsn), + itisSolrResponse.scientificName, commonName, - itisResponse, - itisResponse.updateDate + itisSolrResponse, + itisSolrResponse.updateDate ); }