Compare commits

..

5 commits

Author SHA1 Message Date
02c2f37379 Shows/controller: validate search parameters 2025-02-01 20:33:18 +01:00
a2b747a823 Shows: add pagination to findAll 2025-02-01 20:24:45 +01:00
fd2f2091ee Enable validation 2025-02-01 19:57:18 +01:00
ab5bb50d7b Add pagination dto 2025-01-31 13:20:06 +01:00
98f4e451a9 Nix: run formatter 2025-01-31 13:11:00 +01:00
6 changed files with 84 additions and 15 deletions

View file

@ -14,7 +14,7 @@
}; };
packages = rec { packages = rec {
default = shows-api; default = shows-api;
shows-api = pkgs.callPackage ./package.nix {}; shows-api = pkgs.callPackage ./package.nix { };
}; };
} }
); );

View file

@ -1,7 +1,7 @@
{ { lib
lib, , buildNpmPackage
buildNpmPackage, , nodejs
nodejs, ,
}: }:
buildNpmPackage { buildNpmPackage {
name = "shows-api"; name = "shows-api";
@ -28,7 +28,7 @@ buildNpmPackage {
meta = { meta = {
description = "NestJS API to store info about shows on a MongoDB database"; description = "NestJS API to store info about shows on a MongoDB database";
homepage = "https://git.everest.tailscale/Toast/shows-api"; homepage = "https://git.everest.tailscale/Toast/shows-api";
sourceProvenance = [lib.sourceTypes.fromSource]; sourceProvenance = [ lib.sourceTypes.fromSource ];
mainProgram = "shows-api"; mainProgram = "shows-api";
}; };
} }

View file

@ -1,6 +1,7 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; 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() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
@ -17,6 +18,31 @@ async function bootstrap() {
credentials: true, credentials: true,
origin, 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); await app.listen(process.env.PORT);
} }

15
src/pagination.dto.ts Normal file
View file

@ -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;
}

View file

@ -13,6 +13,14 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ShowsService } from './shows.service'; import { ShowsService } from './shows.service';
import { ShowDto } from './dto/show.dto'; 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') @Controller('shows')
export class ShowsController { export class ShowsController {
@ -36,10 +44,15 @@ export class ShowsController {
} }
@Get() @Get()
async findAll() { async findAll(@Query() pagination: PaginationDto) {
try { try {
const shows = await this.showsService.findAll(); const { page, limit } = pagination;
return { status: 'Ok', shows, totalShows: shows.length }; const serviceResponse = await this.showsService.findAll(page, limit);
return {
status: 'Ok',
shows: serviceResponse.shows,
showCount: serviceResponse.showCount,
};
} catch (error) { } catch (error) {
throw new InternalServerErrorException({ throw new InternalServerErrorException({
status: error.name, status: error.name,
@ -72,15 +85,16 @@ export class ShowsController {
} }
@Get('search') @Get('search')
async search(@Query('query') name: string) { async search(@Query() search: SearchDto) {
try { try {
const shows = await this.showsService.search(name); const { query } = search;
const shows = await this.showsService.search(query);
if (shows.length > 0) { if (shows.length > 0) {
return { status: 'Ok', show: shows, test: shows.length }; return { status: 'Ok', show: shows, test: shows.length };
} else { } else {
throw new NotFoundException({ throw new NotFoundException({
status: 'Error', status: 'Error',
message: `Can't find show matching ${name}`, message: `Can't find show matching ${query}`,
}); });
} }
} catch (error) { } catch (error) {

View file

@ -13,8 +13,22 @@ export class ShowsService {
return show.save(); return show.save();
} }
async findAll(): Promise<Show[]> { async findAll(page: number, showsPerPage: number): Promise<any> {
return this.showModel.find(); // 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<any> { async findId(id: string): Promise<any> {