From 98f4e451a95196a74fa9349b97661cbbe4e78adc Mon Sep 17 00:00:00 2001 From: Toast Date: Fri, 31 Jan 2025 13:11:00 +0100 Subject: [PATCH 1/5] Nix: run formatter --- flake.nix | 2 +- package.nix | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index ca74dc1..e12cfa5 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,7 @@ }; packages = rec { default = shows-api; - shows-api = pkgs.callPackage ./package.nix {}; + shows-api = pkgs.callPackage ./package.nix { }; }; } ); diff --git a/package.nix b/package.nix index 659c30a..4078811 100644 --- a/package.nix +++ b/package.nix @@ -1,7 +1,7 @@ -{ - lib, - buildNpmPackage, - nodejs, +{ lib +, buildNpmPackage +, nodejs +, }: buildNpmPackage { name = "shows-api"; @@ -28,7 +28,7 @@ buildNpmPackage { meta = { description = "NestJS API to store info about shows on a MongoDB database"; homepage = "https://git.everest.tailscale/Toast/shows-api"; - sourceProvenance = [lib.sourceTypes.fromSource]; + sourceProvenance = [ lib.sourceTypes.fromSource ]; mainProgram = "shows-api"; }; } From ab5bb50d7bbb390170a46940cb9dd4adfb97a928 Mon Sep 17 00:00:00 2001 From: Toast Date: Fri, 31 Jan 2025 13:20:06 +0100 Subject: [PATCH 2/5] Add pagination dto --- src/pagination.dto.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/pagination.dto.ts diff --git a/src/pagination.dto.ts b/src/pagination.dto.ts new file mode 100644 index 0000000..fbdd2e1 --- /dev/null +++ b/src/pagination.dto.ts @@ -0,0 +1,15 @@ +import { Type } from "class-transformer"; +import { IsNumber, IsOptional, Min } from "class-validator"; + +export class PaginationDto { + @IsOptional() + @IsNumber() + @Type(() => Number) + @Min(1) + page?: number = 1; + @IsOptional() + @IsNumber() + @Type(() => Number) + @Min(1) + limit?: number = 1; +} From fd2f2091ee3c9ddcdb710b616bdab1c60d4abfaa Mon Sep 17 00:00:00 2001 From: Toast Date: Sat, 1 Feb 2025 19:57:18 +0100 Subject: [PATCH 3/5] Enable validation --- src/main.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 2e23c55..ff80913 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import { Logger } from '@nestjs/common'; +import { BadRequestException, Logger, ValidationPipe } from '@nestjs/common'; +import { ValidationError } from 'class-validator'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -17,6 +18,31 @@ async function bootstrap() { credentials: true, origin, }); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + // https://stackoverflow.com/questions/75581669/customize-error-message-in-nest-js-using-class-validator + exceptionFactory: (validationErrors: ValidationError[] = []) => { + const errors = validationErrors.map((error) => ({ + error: Object.values(error.constraints).join(', '), + })); + + let errorMessage: string; + errors.forEach((value, index) => { + if (index == 0) { + errorMessage = value.error; + } else { + errorMessage = errorMessage.concat(', ', value.error); + } + }); + + return new BadRequestException({ + status: 'Validation Error', + message: errorMessage, + }); + }, + }), + ); await app.listen(process.env.PORT); } From a2b747a82360f9c11c56b3f9a7f75790d016612e Mon Sep 17 00:00:00 2001 From: Toast Date: Sat, 1 Feb 2025 20:24:45 +0100 Subject: [PATCH 4/5] Shows: add pagination to findAll --- src/shows/shows.controller.ts | 12 +++++++++--- src/shows/shows.service.ts | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/shows/shows.controller.ts b/src/shows/shows.controller.ts index 7924d92..8215fa6 100644 --- a/src/shows/shows.controller.ts +++ b/src/shows/shows.controller.ts @@ -13,6 +13,7 @@ import { } from '@nestjs/common'; import { ShowsService } from './shows.service'; import { ShowDto } from './dto/show.dto'; +import { PaginationDto } from 'src/pagination.dto'; @Controller('shows') export class ShowsController { @@ -36,10 +37,15 @@ export class ShowsController { } @Get() - async findAll() { + async findAll(@Query() pagination: PaginationDto) { try { - const shows = await this.showsService.findAll(); - return { status: 'Ok', shows, totalShows: shows.length }; + const { page, limit } = pagination; + const serviceResponse = await this.showsService.findAll(page, limit); + return { + status: 'Ok', + shows: serviceResponse.shows, + showCount: serviceResponse.showCount, + }; } catch (error) { throw new InternalServerErrorException({ status: error.name, diff --git a/src/shows/shows.service.ts b/src/shows/shows.service.ts index 0c28442..76189f8 100644 --- a/src/shows/shows.service.ts +++ b/src/shows/shows.service.ts @@ -13,8 +13,22 @@ export class ShowsService { return show.save(); } - async findAll(): Promise { - return this.showModel.find(); + async findAll(page: number, showsPerPage: number): Promise { + // Default value is 1 and I can't think any reason why you would want + // a single item per page, so if showsPerPage is 1 then just don't do + // any pagination at all + let shows; + if (showsPerPage != 1) { + const skip = (page - 1) * showsPerPage; + shows = await this.showModel.find().skip(skip).limit(showsPerPage); + } else { + shows = await this.showModel.find(); + } + const total = await this.showModel.countDocuments(); + return { + shows: shows, + showCount: total, + }; } async findId(id: string): Promise { From 02c2f373795b69db919f15871920e7b0e8094ae5 Mon Sep 17 00:00:00 2001 From: Toast Date: Sat, 1 Feb 2025 20:33:18 +0100 Subject: [PATCH 5/5] Shows/controller: validate search parameters --- src/shows/shows.controller.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/shows/shows.controller.ts b/src/shows/shows.controller.ts index 8215fa6..86e4ca1 100644 --- a/src/shows/shows.controller.ts +++ b/src/shows/shows.controller.ts @@ -14,6 +14,13 @@ import { import { ShowsService } from './shows.service'; import { ShowDto } from './dto/show.dto'; import { PaginationDto } from 'src/pagination.dto'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class SearchDto { + @IsString() + @IsNotEmpty() + query: string; +} @Controller('shows') export class ShowsController { @@ -78,15 +85,16 @@ export class ShowsController { } @Get('search') - async search(@Query('query') name: string) { + async search(@Query() search: SearchDto) { try { - const shows = await this.showsService.search(name); + const { query } = search; + const shows = await this.showsService.search(query); if (shows.length > 0) { return { status: 'Ok', show: shows, test: shows.length }; } else { throw new NotFoundException({ status: 'Error', - message: `Can't find show matching ${name}`, + message: `Can't find show matching ${query}`, }); } } catch (error) {