diff --git a/apps/entity-service/src/app.module.ts b/apps/entity-service/src/app.module.ts index 7b691ab..a45bad3 100644 --- a/apps/entity-service/src/app.module.ts +++ b/apps/entity-service/src/app.module.ts @@ -2,10 +2,12 @@ import { Module } from "@nestjs/common"; import { DatabaseModule } from "@terramatch-microservices/database"; import { CommonModule } from "@terramatch-microservices/common"; import { HealthModule } from "./health/health.module"; +import { TreesController } from "./trees/trees.controller"; +import { ResearchService } from "./trees/research.service"; @Module({ imports: [DatabaseModule, CommonModule, HealthModule], - controllers: [], - providers: [] + controllers: [TreesController], + providers: [ResearchService] }) export class AppModule {} diff --git a/apps/entity-service/src/trees/dto/scientific-name.dto.ts b/apps/entity-service/src/trees/dto/scientific-name.dto.ts new file mode 100644 index 0000000..a25e5bf --- /dev/null +++ b/apps/entity-service/src/trees/dto/scientific-name.dto.ts @@ -0,0 +1,12 @@ +import { JsonApiAttributes } from "@terramatch-microservices/common/dto/json-api-attributes"; +import { ApiProperty } from "@nestjs/swagger"; +import { JsonApiDto } from "@terramatch-microservices/common/decorators"; + +@JsonApiDto({ type: "treeSpeciesScientificNames", id: "string" }) +export class ScientificNameDto extends JsonApiAttributes { + @ApiProperty({ + description: "The scientific name for this tree species", + example: "Abelia uniflora" + }) + scientificName: string; +} diff --git a/apps/entity-service/src/trees/research.service.ts b/apps/entity-service/src/trees/research.service.ts new file mode 100644 index 0000000..52037fc --- /dev/null +++ b/apps/entity-service/src/trees/research.service.ts @@ -0,0 +1,21 @@ +import { TreeSpeciesResearch } from "@terramatch-microservices/database/entities"; +import { Op } from "sequelize"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class ResearchService { + async searchScientificNames(search: string) { + return await TreeSpeciesResearch.findAll({ + where: { + [Op.or]: [ + // By checking these two, we're limiting the search term to only occurrences at the + // beginning of a word in the scientific name, which tends to lead to better results. + { scientificName: { [Op.like]: `${search}%` } }, + { scientificName: { [Op.like]: `% ${search}%` } } + ] + }, + attributes: ["taxonId", "scientificName"], + limit: 10 + }); + } +} diff --git a/apps/entity-service/src/trees/trees.controller.ts b/apps/entity-service/src/trees/trees.controller.ts new file mode 100644 index 0000000..747e5be --- /dev/null +++ b/apps/entity-service/src/trees/trees.controller.ts @@ -0,0 +1,28 @@ +import { BadRequestException, Controller, Get, Query } from "@nestjs/common"; +import { ResearchService } from "./research.service"; +import { buildJsonApi } from "@terramatch-microservices/common/util"; +import { ScientificNameDto } from "./dto/scientific-name.dto"; +import { ApiOperation } from "@nestjs/swagger"; +import { JsonApiResponse } from "@terramatch-microservices/common/decorators"; + +@Controller("trees/v3") +export class TreesController { + constructor(private readonly researchService: ResearchService) {} + + @Get("scientific-names") + @ApiOperation({ + operationId: "treeScientificNames", + description: "Search scientific names of tree species. Returns up to 10 entries" + }) + @JsonApiResponse({ data: { type: ScientificNameDto } }) + async searchScientificNames(@Query("search") search: string) { + if (search == null) throw new BadRequestException("search query param is required"); + + const document = buildJsonApi(); + for (const treeSpecies of await this.researchService.searchScientificNames(search)) { + document.addData(`${treeSpecies.taxonId}`, new ScientificNameDto(treeSpecies)); + } + + return document.serialize(); + } +} diff --git a/libs/common/src/lib/decorators/json-api-dto.decorator.ts b/libs/common/src/lib/decorators/json-api-dto.decorator.ts index 5e5a5fc..1d62e26 100644 --- a/libs/common/src/lib/decorators/json-api-dto.decorator.ts +++ b/libs/common/src/lib/decorators/json-api-dto.decorator.ts @@ -1,9 +1,9 @@ -import { applyDecorators, SetMetadata } from '@nestjs/common'; +import { applyDecorators, SetMetadata } from "@nestjs/common"; -export const DTO_TYPE_METADATA = 'DTO_TYPE_METADATA'; -export const DTO_ID_METADATA = 'DTO_ID_METADATA'; +export const DTO_TYPE_METADATA = "DTO_TYPE_METADATA"; +export const DTO_ID_METADATA = "DTO_ID_METADATA"; -export type IdType = 'uuid' | 'number'; +export type IdType = "uuid" | "number" | "string"; export type DtoOptions = { type: string; @@ -12,9 +12,7 @@ export type DtoOptions = { * The type of the id for this DTO. Defaults to UUID */ id?: IdType; -} +}; -export const JsonApiDto = (options: DtoOptions) => applyDecorators( - SetMetadata(DTO_TYPE_METADATA, options.type), - SetMetadata(DTO_ID_METADATA, options.id ?? 'uuid') -); +export const JsonApiDto = (options: DtoOptions) => + applyDecorators(SetMetadata(DTO_TYPE_METADATA, options.type), SetMetadata(DTO_ID_METADATA, options.id ?? "uuid"));