Pushing errors back to frontend
Some checks failed
Music Collection CI Workflow / test (./backend) (push) Failing after 26s
Music Collection CI Workflow / test (./frontend) (push) Successful in 38s
Music Collection CI Workflow / build-and-push-images (./backend/Dockerfile, git.anatid.net/tabris/music-collection-backend, ./backend) (push) Has been skipped
Music Collection CI Workflow / build-and-push-images (./frontend/Dockerfile, git.anatid.net/tabris/music-collection-frontend, ./frontend) (push) Has been skipped
Music Collection CI Workflow / deploy (push) Has been skipped

This commit is contained in:
Phill Pover 2025-04-07 10:48:47 +01:00
parent 040675bb5d
commit 2214fa32f5
9 changed files with 145 additions and 93 deletions

View File

@ -31,7 +31,9 @@ export class AlbumController {
@Post() @Post()
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createAlbumDto: CreateAlbumDto): Promise<Album | string | null> { async create(
@Body() createAlbumDto: CreateAlbumDto,
): Promise<Album | string | null> {
return this.albumService.create(createAlbumDto); return this.albumService.create(createAlbumDto);
} }

View File

@ -13,40 +13,45 @@ export class AlbumService {
) {} ) {}
findAll(): Promise<Album[] | string | null> { findAll(): Promise<Album[] | string | null> {
return this.albumRepository.find({ return this.albumRepository
order: { .find({
artist: 'ASC', order: {
title: 'ASC', artist: 'ASC',
}, title: 'ASC',
}).then((albums) => { },
return albums; })
}) .then((albums) => {
.catch((error) => { return albums;
return `There was a problem getting the list of albums: ${error}` })
}); .catch((error) => {
return `There was a problem getting the list of albums: ${error}`;
});
} }
findOneById(id: number): Promise<Album | string | null> { findOneById(id: number): Promise<Album | string | null> {
return this.albumRepository.findOne({ return this.albumRepository
where: { .findOne({
id: id, where: {
}, id: id,
relations: {
songs: true,
},
order: {
songs: {
trackNumber: 'ASC',
}, },
}, relations: {
}).then((album) => { songs: true,
return album; },
}) order: {
.catch((error) => { songs: {
return `There was a problem creating the Album identified by ID ${id}: ${error}` trackNumber: 'ASC',
}).finally(() => { },
return `There was a problem creating the Album identified by ID ${id}` },
}); })
.then((album) => {
return album;
})
.catch((error) => {
return `There was a problem creating the Album identified by ID ${id}: ${error}`;
})
.finally(() => {
return `There was a problem creating the Album identified by ID ${id}`;
});
} }
async create(createAlbumDto: CreateAlbumDto): Promise<Album | string | null> { async create(createAlbumDto: CreateAlbumDto): Promise<Album | string | null> {
@ -55,12 +60,13 @@ export class AlbumService {
artist: createAlbumDto.artist, artist: createAlbumDto.artist,
genre: createAlbumDto.genre, genre: createAlbumDto.genre,
}); });
return await this.albumRepository.save(album) return await this.albumRepository
.save(album)
.then((savedAlbum) => { .then((savedAlbum) => {
return savedAlbum; return savedAlbum;
}) })
.catch((error) => { .catch((error) => {
return `There was a problem creating the Album (${createAlbumDto.title} by ${createAlbumDto.artist} (${createAlbumDto.genre})): ${error}` return `There was a problem creating the Album (${createAlbumDto.title} by ${createAlbumDto.artist} (${createAlbumDto.genre})): ${error}`;
}); });
} }
@ -100,12 +106,13 @@ export class AlbumService {
} }
async remove(id: number): Promise<DeleteResult | string | null> { async remove(id: number): Promise<DeleteResult | string | null> {
return await this.albumRepository.delete(id) return await this.albumRepository
.then((deleteResult) => { .delete(id)
return deleteResult; .then((deleteResult) => {
}) return deleteResult;
.catch((error) => { })
return `There was a problem deleting the Album identified by ID ${id}: ${error}` .catch((error) => {
}); return `There was a problem deleting the Album identified by ID ${id}: ${error}`;
});
} }
} }

View File

@ -1,3 +1,5 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateAlbumDto { export class CreateAlbumDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()

View File

@ -1,5 +1,6 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class UpdateAlbumDto { export class UpdateAlbumDto {
@Column()
@IsNumber() @IsNumber()
id: number; id: number;
@ -13,7 +14,6 @@ export class UpdateAlbumDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
genre: string; genre: string;

View File

@ -1,5 +1,5 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common'; import { UnprocessableEntityException, ValidationError, ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
async function bootstrap() { async function bootstrap() {
@ -13,16 +13,53 @@ async function bootstrap() {
allowedHeaders: allowedHeaders:
'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept, Observe', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept, Observe',
}); });
app.useGlobalPipes(new ValidationPipe({ app.useGlobalPipes(
transform: true, new ValidationPipe({
exceptionFactory: (errors) => { transform: true,
const result = errors.map((error) => ({ whitelist: true,
property: error.property, exceptionFactory: (validationErrors: ValidationError[] = []) => {
message: error.constraints[Object.keys(error.constraints)[0]], const getPrettyClassValidatorErrors = (
})); validationErrors: ValidationError[],
return new UnprocessableEntityException(result); parentProperty = '',
): Array<{ property: string; errors: string[] }> => {
const errors : any[] = [];
const getValidationErrorsRecursively = (
validationErrors: ValidationError[],
parentProperty = '',
) => {
for (const error of validationErrors) {
const propertyPath = parentProperty
? `${parentProperty}.${error.property}`
: error.property;
if (error.constraints) {
errors.push({
property: propertyPath,
errors: Object.values(error.constraints),
});
}
if (error.children?.length) {
getValidationErrorsRecursively(error.children, propertyPath);
}
}
};
getValidationErrorsRecursively(validationErrors, parentProperty);
return errors;
};
const errors = getPrettyClassValidatorErrors(validationErrors);
return new UnprocessableEntityException({
message: 'validation error',
errors: errors,
});
}, },
})); }),
);
await app.listen(process.env.PORT ?? 3000); await app.listen(process.env.PORT ?? 3000);
} }
bootstrap(); bootstrap();

View File

@ -1,21 +1,16 @@
import { Entity, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class CreateSongDto { export class CreateSongDto {
@Column({ unique: true })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
title: string; title: string;
@Column()
@IsNumber() @IsNumber()
duration: number; duration: number;
@Column()
@IsNumber() @IsNumber()
trackNumber: number; trackNumber: number;
@Column()
@IsNumber() @IsNumber()
albumId: number; albumId: number;
} }

View File

@ -1,22 +1,19 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
export class UpdateSongDto { export class UpdateSongDto {
@Column()
@IsNumber() @IsNumber()
id: number; id: number;
@Column({ unique: true })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
title: string; title: string;
@Column()
@IsNumber() @IsNumber()
duration: number; duration: number;
@Column()
@IsNumber() @IsNumber()
trackNumber: number; trackNumber: number;
@Column()
@IsNumber() @IsNumber()
albumId: number; albumId: number;
} }

View File

@ -31,7 +31,9 @@ export class SongController {
@Post() @Post()
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createSongDto: CreateSongDto): Promise<Song | string | null> { async create(
@Body() createSongDto: CreateSongDto,
): Promise<Song | string | null> {
return this.songService.create(createSongDto); return this.songService.create(createSongDto);
} }

View File

@ -16,25 +16,29 @@ export class SongService {
) {} ) {}
findAll(): Promise<Song[] | string | null> { findAll(): Promise<Song[] | string | null> {
return this.songRepository.find({ return this.songRepository
order: { .find({
trackNumber: 'ASC', order: {
}, trackNumber: 'ASC',
}).then((songs) => { },
return songs; })
}) .then((songs) => {
.catch((error) => { return songs;
return `There was a problem getting the list of songs: ${error}` })
}); .catch((error) => {
return `There was a problem getting the list of songs: ${error}`;
});
} }
findOneById(id: number): Promise<Song | string | null> { findOneById(id: number): Promise<Song | string | null> {
return this.songRepository.findOneBy({ id: id }).then((albums) => { return this.songRepository
return albums; .findOneBy({ id: id })
}) .then((albums) => {
.catch((error) => { return albums;
return `There was a problem getting the song identified by ID ${id}: ${error}` })
}); .catch((error) => {
return `There was a problem getting the song identified by ID ${id}: ${error}`;
});
} }
async create(createSongDto: CreateSongDto): Promise<Song | string | null> { async create(createSongDto: CreateSongDto): Promise<Song | string | null> {
@ -47,18 +51,23 @@ export class SongService {
song.duration = createSongDto.duration; song.duration = createSongDto.duration;
song.trackNumber = createSongDto.trackNumber; song.trackNumber = createSongDto.trackNumber;
song.album = album; song.album = album;
return this.songRepository.save(song).then((albums) => { return this.songRepository
return albums; .save(song)
}) .then((albums) => {
.catch((error) => { return albums;
return `There was a problem creating the song (${createSongDto.trackNumber} ${createSongDto.title} (${createSongDto.duration}s) on the Album ${album.title} by ${album.artist}): ${error}` })
}); .catch((error) => {
return `There was a problem creating the song (${createSongDto.trackNumber} ${createSongDto.title} (${createSongDto.duration}s) on the Album ${album.title} by ${album.artist}): ${error}`;
});
} else { } else {
throw new Error(`Unable to find Album with ID ${createSongDto.albumId}`); throw new Error(`Unable to find Album with ID ${createSongDto.albumId}`);
} }
} }
async update(id: number, updateSongDto: UpdateSongDto): Promise<Song | string | null> { async update(
id: number,
updateSongDto: UpdateSongDto,
): Promise<Song | string | null> {
if (id == updateSongDto.id) { if (id == updateSongDto.id) {
const album = await this.albumRepository.findOneBy({ const album = await this.albumRepository.findOneBy({
id: updateSongDto.albumId, id: updateSongDto.albumId,
@ -91,12 +100,13 @@ export class SongService {
} }
async remove(id: number): Promise<DeleteResult | string | null> { async remove(id: number): Promise<DeleteResult | string | null> {
return await this.songRepository.delete(id) return await this.songRepository
.then((deleteResult) => { .delete(id)
return deleteResult; .then((deleteResult) => {
}) return deleteResult;
.catch((error) => { })
return `There was a problem deleting the Song identified by ID ${id}: ${error}` .catch((error) => {
}); return `There was a problem deleting the Song identified by ID ${id}: ${error}`;
});
} }
} }