From 0f98d9eb820090f96192db2c678d52e163d9e33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Scheibel=20de=20Almada?= Date: Wed, 12 Jul 2023 15:47:06 -0300 Subject: [PATCH] feat: add initial swagger docs --- package.json | 1 + src/app.controller.spec.ts | 22 ---- src/app.controller.ts | 12 --- src/app.module.ts | 7 +- src/app.service.ts | 8 -- src/fishes/dto/fish.dto.ts | 97 ++++++++++++++++++ src/fishes/fishes.controller.ts | 130 ++++++++++++++++-------- src/fishes/fishes.service.ts | 2 + src/fishes/interfaces/fish.interface.ts | 33 ++++++ src/fishes/schemas/fish.schema.ts | 45 ++------ src/main.ts | 16 +++ test/app.e2e-spec.ts | 24 ----- yarn.lock | 35 +++++-- 13 files changed, 275 insertions(+), 157 deletions(-) delete mode 100644 src/app.controller.spec.ts delete mode 100644 src/app.controller.ts delete mode 100644 src/app.service.ts create mode 100644 src/fishes/dto/fish.dto.ts delete mode 100644 test/app.e2e-spec.ts diff --git a/package.json b/package.json index 8b14b93..b1ce1bd 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@nestjs/core": "^10.0.0", "@nestjs/mongoose": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "7.1.1", "csv-parse": "5.4.0", "mongoose": "^7.3.1", "reflect-metadata": "^0.1.13", diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 628c283..24c89aa 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,9 +4,6 @@ import { MongooseModule } from '@nestjs/mongoose'; import { FishesModule } from './fishes/fishes.module'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - @Module({ imports: [ ConfigModule.forRoot(), @@ -16,7 +13,7 @@ import { AppService } from './app.service'; ), FishesModule, ], - controllers: [AppController], - providers: [AppService], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/fishes/dto/fish.dto.ts b/src/fishes/dto/fish.dto.ts new file mode 100644 index 0000000..4cabe62 --- /dev/null +++ b/src/fishes/dto/fish.dto.ts @@ -0,0 +1,97 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IFishLocation } from '../interfaces/fish.interface'; + +const monthProperty = { + oneOf: [ + { + type: 'array', + items: { + type: 'string', + enum: [ + 'All day', + '9 AM - 4 PM', + '4 PM - 9 AM', + '9 PM - 4 AM', + '4 AM - 9 PM', + ], + }, + }, + { type: 'undefined' }, + ], +}; +const availabilityProperties = { + january: monthProperty, + february: monthProperty, + march: monthProperty, + april: monthProperty, + may: monthProperty, + june: monthProperty, + july: monthProperty, + august: monthProperty, + september: monthProperty, + october: monthProperty, + november: monthProperty, + december: monthProperty, +}; + +class Availability { + @ApiProperty({ + type: 'object', + properties: availabilityProperties, + }) + northern_hemisphere: Object; + + @ApiProperty({ + type: 'object', + properties: availabilityProperties, + }) + southern_hemisphere: Object; +} + +export class FishDto { + @ApiProperty() + name: string; + + @ApiProperty() + icon_image: string; + + @ApiProperty() + critterpedia_image: string; + + @ApiProperty() + furniture_image: string; + + @ApiProperty() + minimum_catches_to_spawn: number; + + @ApiProperty() + sale_price: number; + + @ApiProperty() + spawn_frequency: string; + + @ApiProperty({ + enum: [ + 'River', + 'Pond', + 'River (clifftop)', + 'River (mouth)', + 'Sea', + 'Pier', + 'Sea (rainy days)', + ], + }) + location: IFishLocation; + + @ApiProperty() + shadow_size: string; + + @ApiProperty() + blathers_description: string; + + @ApiProperty() + catch_phrase: string; + + @ApiProperty() + availability: Availability; +} diff --git a/src/fishes/fishes.controller.ts b/src/fishes/fishes.controller.ts index 1e038c6..0202ccc 100644 --- a/src/fishes/fishes.controller.ts +++ b/src/fishes/fishes.controller.ts @@ -6,68 +6,107 @@ import { HttpException, HttpStatus, } from '@nestjs/common'; +import { + ApiOperation, + ApiTags, + ApiOkResponse, + ApiQuery, + ApiBadRequestResponse, +} from '@nestjs/swagger'; import { FishesService } from './fishes.service'; -import { Fish, IMonth } from './schemas/fish.schema'; -import { IQueryParams } from './interfaces/fish.interface'; +import { Fish } from './schemas/fish.schema'; +import { IQueryParams, IMonth } from './interfaces/fish.interface'; +import { FishDto } from './dto/fish.dto'; + +const hemispheres = ['northern_hemisphere', 'southern_hemisphere']; +const months = [ + 'january', + 'february', + 'march', + 'april', + 'may', + 'june', + 'july', + 'august', + 'september', + 'october', + 'november', + 'december', +]; +const times = [ + '12 AM', + '1 AM', + '2 AM', + '3 AM', + '4 AM', + '5 AM', + '6 AM', + '7 AM', + '8 AM', + '9 AM', + '10 AM', + '11 AM', + '12 PM', + '1 PM', + '2 PM', + '3 PM', + '4 PM', + '5 PM', + '6 PM', + '7 PM', + '8 PM', + '9 PM', + '10 PM', + '11 PM', +]; + +@ApiTags('Fishes') @Controller('fishes') export class FishesController { constructor(private fishesService: FishesService) {} // Query params verifiers private isOfTypeIMonth(month: string): month is IMonth { - return [ - 'january', - 'february', - 'march', - 'april', - 'may', - 'june', - 'july', - 'august', - 'september', - 'october', - 'november', - 'december', - ].includes(month); + return months.includes(month); } private isValidHemisphere(hemisphere: string): boolean { - return ['northern_hemisphere', 'southern_hemisphere'].includes(hemisphere); + return hemispheres.includes(hemisphere); } private isValidTime(time: string): boolean { - return [ - '12 AM', - '1 AM', - '2 AM', - '3 AM', - '4 AM', - '5 AM', - '6 AM', - '7 AM', - '8 AM', - '9 AM', - '10 AM', - '11 AM', - '12 PM', - '1 PM', - '2 PM', - '3 PM', - '4 PM', - '5 PM', - '6 PM', - '7 PM', - '8 PM', - '9 PM', - '10 PM', - '11 PM', - ].includes(time); + return times.includes(time); } // Routes @Get() + @ApiOperation({ summary: 'Obtain all fishes' }) + @ApiQuery({ + name: 'hemisphere', + enum: hemispheres, + required: false, + }) + @ApiQuery({ + name: 'month', + enum: months, + required: false, + }) + @ApiQuery({ + name: 'time', + enum: times, + required: false, + }) + @ApiOkResponse({ + description: "The found fish records by request's criteria", + type: FishDto, + isArray: true, + }) + @ApiBadRequestResponse({ + description: + 'Invalid query param informed or searching by active hours without informing a month', + }) public getFishes(@Query() query: IQueryParams): Promise { if (query.month && !this.isOfTypeIMonth(query.month)) throw new HttpException( @@ -97,6 +136,11 @@ export class FishesController { } @Get(':name') + @ApiOkResponse({ + description: 'The found fish record, or null, if not found', + type: FishDto, + }) + @ApiOperation({ summary: 'Obtain all information from a specific fish' }) public getFish(@Param('name') name: string): Promise { return this.fishesService.findOne(name); } diff --git a/src/fishes/fishes.service.ts b/src/fishes/fishes.service.ts index 6a221eb..d325f7d 100644 --- a/src/fishes/fishes.service.ts +++ b/src/fishes/fishes.service.ts @@ -49,6 +49,8 @@ export class FishesService { public find(queryParams: IQueryParams): Promise { const buildQuery = () => { + if (!queryParams.month) return {}; + // If request informed a desired hour, we must obtain all possible active hours that contemplates it let formattedTime; if (queryParams.time) formattedTime = this.formatTime(queryParams.time); diff --git a/src/fishes/interfaces/fish.interface.ts b/src/fishes/interfaces/fish.interface.ts index 2899d82..2a55ef6 100644 --- a/src/fishes/interfaces/fish.interface.ts +++ b/src/fishes/interfaces/fish.interface.ts @@ -1,3 +1,36 @@ +export type IFishLocation = + | 'River' + | 'Pond' + | 'River (clifftop)' + | 'River (mouth)' + | 'Sea' + | 'Pier' + | 'Sea (rainy days)'; + +export type IMonth = + | 'january' + | 'february' + | 'march' + | 'april' + | 'may' + | 'june' + | 'july' + | 'august' + | 'september' + | 'october' + | 'november' + | 'december'; + +export type IPeriodOfDay = + | 'All day' + | '9 AM - 4 PM' + | '4 PM - 9 AM' + | '9 PM - 4 AM' + | '4 AM - 9 PM'; + +export type IAvailability = { + [K in T]?: U; +}; export interface IQueryParams { hemisphere?: string; month?: string; diff --git a/src/fishes/schemas/fish.schema.ts b/src/fishes/schemas/fish.schema.ts index 4bd1027..1e3ec44 100644 --- a/src/fishes/schemas/fish.schema.ts +++ b/src/fishes/schemas/fish.schema.ts @@ -1,43 +1,16 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument } from 'mongoose'; +import { + IMonth, + IFishLocation, + IPeriodOfDay, + IAvailability, +} from '../interfaces/fish.interface'; + export type FishDocument = HydratedDocument; -type IFishLocation = - | 'River' - | 'Pond' - | 'River (clifftop)' - | 'River (mouth)' - | 'Sea' - | 'Pier' - | 'Sea (rainy days)'; - -type IPeriodOfDay = - | 'All day' - | '9 AM - 4 PM' - | '4 PM - 9 AM' - | '9 PM - 4 AM' - | '4 AM - 9 PM'; - -export type IMonth = - | 'january' - | 'february' - | 'march' - | 'april' - | 'may' - | 'june' - | 'july' - | 'august' - | 'september' - | 'october' - | 'november' - | 'december'; - -type IAvailability = { - [K in T]?: U; -}; - -@Schema() +@Schema({ versionKey: false }) class Availability { @Prop({ required: true, type: Object }) northern_hemisphere: IAvailability; @@ -46,7 +19,7 @@ class Availability { southern_hemisphere: IAvailability; } -@Schema() +@Schema({ versionKey: false }) export class Fish { @Prop({ required: true }) name: string; diff --git a/src/main.ts b/src/main.ts index 13cad38..03a5a07 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,24 @@ import { NestFactory } from '@nestjs/core'; +import { + SwaggerModule, + DocumentBuilder, + SwaggerDocumentOptions, +} from '@nestjs/swagger'; + import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + const config = new DocumentBuilder() + .setTitle('DodoApi') + .setDescription("Animal Crossing's RESTful API") + .setVersion('0.1.1') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + await app.listen(3000); } bootstrap(); diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts deleted file mode 100644 index 50cda62..0000000 --- a/test/app.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/yarn.lock b/yarn.lock index 4dac8d3..ce1ebad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -753,6 +753,11 @@ path-to-regexp "3.2.0" tslib "2.6.0" +"@nestjs/mapped-types@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz#c8a090a8d22145b85ed977414c158534210f2e4f" + integrity sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg== + "@nestjs/mongoose@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@nestjs/mongoose/-/mongoose-10.0.0.tgz#5ce9e4b817847e83afb690db04eb106acaa43b8a" @@ -780,6 +785,17 @@ jsonc-parser "3.2.0" pluralize "8.0.0" +"@nestjs/swagger@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-7.1.1.tgz#67285035a58a32059f219532b449addf81f8e513" + integrity sha512-gIG1aVCegZlIppXZizKHqWkqZvQkvptTBR1C5CzZoDwGoVVKJBmJ2i9FAcsnzzb0j7hncFKhcBuWYOBJOsCvug== + dependencies: + "@nestjs/mapped-types" "2.0.2" + js-yaml "4.1.0" + lodash "4.17.21" + path-to-regexp "3.2.0" + swagger-ui-dist "5.1.0" + "@nestjs/testing@^10.0.0": version "10.0.5" resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.0.5.tgz#31cac7b9816351dff7706b3ff9af0387cf608f4b" @@ -3322,6 +3338,13 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -3330,13 +3353,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -4507,6 +4523,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swagger-ui-dist@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.1.0.tgz#b01b3be06bebb2566b2df586c1632d502ec792ad" + integrity sha512-c1KmAjuVODxw+vwkNLALQZrgdlBAuBbr2xSPfYrJgseEi7gFKcTvShysPmyuDI4kcUa1+5rFpjWvXdusKY74mg== + symbol-observable@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"