diff --git a/src/pokemon/dtos/get-pokemon-by-name.query.ts b/src/pokemon/dtos/get-pokemon-by-name.query.ts index 8bb36a2..9d8f760 100644 --- a/src/pokemon/dtos/get-pokemon-by-name.query.ts +++ b/src/pokemon/dtos/get-pokemon-by-name.query.ts @@ -1,8 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class GetPokemonByNameQuery { - @ApiProperty({ type: 'string' }) + @ApiProperty({ + type: 'string', + description: + 'pokemon name, must be exact and must be the name of a first-generation pokemon', + }) @IsString() + @IsNotEmpty() name: string; } diff --git a/src/pokemon/dtos/get-pokemon-by-name.response.ts b/src/pokemon/dtos/get-pokemon-by-name.response.ts new file mode 100644 index 0000000..7c07a20 --- /dev/null +++ b/src/pokemon/dtos/get-pokemon-by-name.response.ts @@ -0,0 +1,64 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, IsNumber, IsString } from 'class-validator'; + +class PokemonSpeciesResponse { + @ApiProperty({ + type: 'string', + description: 'species name', + }) + @IsString() + name: string; + + @ApiProperty({ + type: 'string', + description: 'species URL', + }) + @IsString() + url: string; +} + +export class GetPokemonByNameResponse { + @ApiProperty({ + type: 'string', + description: 'pokemon name', + }) + @IsString() + name: string; + + @ApiProperty({ + type: 'number', + description: 'pokemon ID (from 1 to 151)', + }) + @IsNumber() + id: number; + + @ApiProperty({ + type: 'number', + description: 'height', + }) + @IsNumber() + height: number; + + @ApiProperty({ + type: 'number', + description: 'weight', + }) + @IsNumber() + weight: number; + + @ApiProperty({ + type: [String], + description: 'pokemon ID (from 1 to 151)', + }) + @IsArray() + @IsString({ each: true }) + types: string[]; + + @ApiProperty({ + type: () => PokemonSpeciesResponse, + description: 'pokemon species', + }) + @Type(() => PokemonSpeciesResponse) + species: PokemonSpeciesResponse; +} diff --git a/src/pokemon/pokemon.controller.ts b/src/pokemon/pokemon.controller.ts index c375be2..5ce7751 100644 --- a/src/pokemon/pokemon.controller.ts +++ b/src/pokemon/pokemon.controller.ts @@ -1,64 +1,64 @@ -export var pokemonApiUrl = 'http://pokeapi.co/api/v2'; - import { Controller, Get, HttpException, - Inject, + InternalServerErrorException, NotFoundException, - Param, - Post, Query, - UseInterceptors, } from '@nestjs/common'; -import { PokemonService } from './pokemon.service'; -import { GetPokemonByNameQuery } from './dtos/get-pokemon-by-name.query'; -import { Pokemon } from './types/pokemon'; import { + ApiBadRequestResponse, ApiInternalServerErrorResponse, + ApiNotFoundResponse, ApiQuery, ApiResponse, ApiTags, } from '@nestjs/swagger'; +import { GetPokemonByNameQuery } from './dtos/get-pokemon-by-name.query'; +import { GetPokemonByNameResponse } from './dtos/get-pokemon-by-name.response'; +import { PokemonService } from './pokemon.service'; +import { PokemonNotFoundError } from './types/error'; +import { Pokemon } from './types/pokemon'; @Controller('pokemon') @ApiTags('pokemon') export class PokemonController { - constructor(private pokemonService: PokemonService) {} + constructor(private readonly pokemonService: PokemonService) {} - @Post('pokemon') + @Get('pokemon') @ApiQuery({ type: GetPokemonByNameQuery, description: 'Lookup string for pokemons (match against pokemon name)', }) @ApiResponse({ status: 200, - description: 'Returns pokemons whose name matches a string query param', + type: GetPokemonByNameResponse, + description: + 'Returns a pokemon whose name exactly matches the string query param', + }) + @ApiBadRequestResponse({ + description: 'Returned if query param "name" is empty', + }) + @ApiNotFoundResponse({ + description: + 'Returned if no first-generation pokemon was found for this name', }) @ApiInternalServerErrorResponse({ - description: 'This will never happen, trust me', + description: 'Returned if any other error is encountered', }) async GetPokemonByNameController( - @Query() name: any, - ): Promise { - if (name === null) return; - - name == null - ? name.trim() != '' - ? ((name = name), - (pokemonApiUrl = pokemonApiUrl + '/'), - (pokemonApiUrl = pokemonApiUrl + name)) - : ((pokemonApiUrl = pokemonApiUrl + '"?offset=20"'), - (pokemonApiUrl = pokemonApiUrl + '&limit=20')) - : ((pokemonApiUrl = pokemonApiUrl + '"?offset=20"'), - (pokemonApiUrl = pokemonApiUrl + '&limit=20')); - - console.log('Printing name for debug : ', name); - - const myPokemon = await this.pokemonService.findPokemonByNameOrFail(name); - - console.log('Printing name for debug : ', myPokemon); - - return myPokemon; + @Query() { name }: GetPokemonByNameQuery, + ): Promise { + try { + return await this.pokemonService.findPokemonByNameOrFail(name); + } catch (error) { + if (error instanceof PokemonNotFoundError) { + throw new NotFoundException(error); + } else if (error instanceof HttpException) { + throw error; + } else { + throw new InternalServerErrorException(error); + } + } } } diff --git a/src/pokemon/pokemon.service.ts b/src/pokemon/pokemon.service.ts index f759b38..363b631 100644 --- a/src/pokemon/pokemon.service.ts +++ b/src/pokemon/pokemon.service.ts @@ -1,21 +1,32 @@ import { Injectable } from '@nestjs/common'; -import got, { Got } from 'got'; +import got from 'got'; import { Pokemon } from './types/pokemon'; -import { pokemonApiUrl } from './pokemon.controller'; import { PokemonSpecies } from './types/species'; +import { PokemonNotFoundError, UnexpectedError } from './types/error'; + +const POKEMON_API_URL = 'http://pokeapi.co/api/v2'; +const FIRST_GENERATION_POKEMON_LAST_ID = 151; @Injectable() export class PokemonService { - private httpClient: Got; - constructor() { - this.httpClient = got.extend({ - prefixUrl: pokemonApiUrl, - responseType: 'json', - throwHttpErrors: false, - }); - } + private readonly httpClient = got.extend({ + prefixUrl: POKEMON_API_URL, + responseType: 'json', + throwHttpErrors: false, + }); async findPokemonByNameOrFail(pokemonName: string): Promise { + try { + return this.findPokemon(pokemonName); + } catch (error) { + if (error instanceof PokemonNotFoundError) { + throw error; + } + throw new UnexpectedError(error); + } + } + + async findPokemon(pokemonName: string): Promise { type GetPokemonResponse = { name: string; id: number; @@ -25,32 +36,34 @@ export class PokemonService { species: PokemonSpecies; }; - return await this.httpClient + return this.httpClient .get(`pokemon/${pokemonName}`) .then((response) => response.body as unknown as GetPokemonResponse) - .then((body) => { + .then(async (body) => { if (!body.id) { - return null; + throw new PokemonNotFoundError( + `No pokemon found for name '${pokemonName}'`, + ); } - const types = body.types - .map((type) => type.type) - .map((type) => type.name); + if (body.id > FIRST_GENERATION_POKEMON_LAST_ID) { + throw new PokemonNotFoundError( + `Pokemon '${pokemonName}' is not first-generation: ID ${body.id} > ${FIRST_GENERATION_POKEMON_LAST_ID}`, + ); + } + // remap PokeAPI stats to a 'PokemonSpecs' object return { name: body.name, id: body.id, height: body.height, weight: body.weight, - types, + types: body.types.map((type) => type.type.name), species: { name: body.species.name, url: body.species.url, }, }; - }) - .catch((error) => { - throw error; }); } } diff --git a/src/pokemon/types/error.ts b/src/pokemon/types/error.ts new file mode 100644 index 0000000..c0a8b18 --- /dev/null +++ b/src/pokemon/types/error.ts @@ -0,0 +1,3 @@ +export class PokemonNotFoundError extends Error {} + +export class UnexpectedError extends Error {}