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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { UnprocessableEntityException, ValidationError, ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
@ -13,16 +13,53 @@ async function bootstrap() {
allowedHeaders:
'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept, Observe',
});
app.useGlobalPipes(new ValidationPipe({
transform: true,
exceptionFactory: (errors) => {
const result = errors.map((error) => ({
property: error.property,
message: error.constraints[Object.keys(error.constraints)[0]],
}));
return new UnprocessableEntityException(result);
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
exceptionFactory: (validationErrors: ValidationError[] = []) => {
const getPrettyClassValidatorErrors = (
validationErrors: ValidationError[],
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);
}
bootstrap();

View File

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

View File

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

View File

@ -31,7 +31,9 @@ export class SongController {
@Post()
@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);
}

View File

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