Merge branch 'main' into feature/adc/#243-leaderboard
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
ConflictException,
|
||||
Controller,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
||||
@@ -21,16 +21,25 @@ import { Prisma, Album } from '@prisma/client';
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Album as _Album } from 'src/_gen/prisma-class/album';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
|
||||
@Controller('album')
|
||||
@ApiTags('album')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class AlbumController {
|
||||
static filterableFields: string[] = ['+id', 'name', '+artistId'];
|
||||
static includableFields: IncludeMap<Prisma.AlbumInclude> = {
|
||||
artist: true,
|
||||
Song: true,
|
||||
};
|
||||
|
||||
constructor(private readonly albumService: AlbumService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ description: "Register a new album, should not be used by frontend"})
|
||||
@ApiOperation({
|
||||
description: 'Register a new album, should not be used by frontend',
|
||||
})
|
||||
async create(@Body() createAlbumDto: CreateAlbumDto) {
|
||||
try {
|
||||
return await this.albumService.createAlbum({
|
||||
@@ -47,7 +56,7 @@ export class AlbumController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: "Delete an album by id"})
|
||||
@ApiOperation({ description: 'Delete an album by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.albumService.deleteAlbum({ id });
|
||||
@@ -58,11 +67,12 @@ export class AlbumController {
|
||||
|
||||
@Get()
|
||||
@ApiOkResponsePlaginated(_Album)
|
||||
@ApiOperation({ description: "Get all albums paginated"})
|
||||
@ApiOperation({ description: 'Get all albums paginated' })
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(AlbumController.filterableFields)
|
||||
where: Prisma.AlbumWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Album>> {
|
||||
@@ -70,15 +80,23 @@ export class AlbumController {
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
include: mapInclude(include, req, AlbumController.includableFields),
|
||||
});
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: "Get an album by id"})
|
||||
@ApiOkResponse({ type: _Album})
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
const res = await this.albumService.album({ id });
|
||||
@ApiOperation({ description: 'Get an album by id' })
|
||||
@ApiOkResponse({ type: _Album })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.albumService.album(
|
||||
{ id },
|
||||
mapInclude(include, req, AlbumController.includableFields),
|
||||
);
|
||||
|
||||
if (res === null) throw new NotFoundException('Album not found');
|
||||
return res;
|
||||
|
||||
@@ -14,9 +14,11 @@ export class AlbumService {
|
||||
|
||||
async album(
|
||||
albumWhereUniqueInput: Prisma.AlbumWhereUniqueInput,
|
||||
include?: Prisma.AlbumInclude,
|
||||
): Promise<Album | null> {
|
||||
return this.prisma.album.findUnique({
|
||||
where: albumWhereUniqueInput,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,14 +28,16 @@ export class AlbumService {
|
||||
cursor?: Prisma.AlbumWhereUniqueInput;
|
||||
where?: Prisma.AlbumWhereInput;
|
||||
orderBy?: Prisma.AlbumOrderByWithRelationInput;
|
||||
include?: Prisma.AlbumInclude;
|
||||
}): Promise<Album[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { skip, take, cursor, where, orderBy, include } = params;
|
||||
return this.prisma.album.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOkResponse({ description: 'Return a hello world message, used as a health route' })
|
||||
@ApiOkResponse({
|
||||
description: 'Return a hello world message, used as a health route',
|
||||
})
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
ConflictException,
|
||||
Controller,
|
||||
@@ -14,26 +13,42 @@ import {
|
||||
Query,
|
||||
Req,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateArtistDto } from './dto/create-artist.dto';
|
||||
import { Request } from 'express';
|
||||
import { ArtistService } from './artist.service';
|
||||
import { Prisma, Artist } from '@prisma/client';
|
||||
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Artist as _Artist} from 'src/_gen/prisma-class/artist';
|
||||
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { Public } from 'src/auth/public';
|
||||
|
||||
@Controller('artist')
|
||||
@ApiTags('artist')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ArtistController {
|
||||
static filterableFields = ['+id', 'name'];
|
||||
static includableFields: IncludeMap<Prisma.ArtistInclude> = {
|
||||
Song: true,
|
||||
Album: true,
|
||||
};
|
||||
|
||||
constructor(private readonly service: ArtistService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ description: "Register a new artist, should not be used by frontend"})
|
||||
@ApiOperation({
|
||||
description: 'Register a new artist, should not be used by frontend',
|
||||
})
|
||||
async create(@Body() dto: CreateArtistDto) {
|
||||
try {
|
||||
return await this.service.create(dto);
|
||||
@@ -43,7 +58,7 @@ export class ArtistController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: "Delete an artist by id"})
|
||||
@ApiOperation({ description: 'Delete an artist by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.service.delete({ id });
|
||||
@@ -53,8 +68,9 @@ export class ArtistController {
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@ApiOperation({ description: "Get an artist's illustration"})
|
||||
@ApiNotFoundResponse({ description: "Artist or illustration not found"})
|
||||
@ApiOperation({ description: "Get an artist's illustration" })
|
||||
@ApiNotFoundResponse({ description: 'Artist or illustration not found' })
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
const artist = await this.service.get({ id });
|
||||
if (!artist) throw new NotFoundException('Artist not found');
|
||||
@@ -71,12 +87,13 @@ export class ArtistController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ description: "Get all artists paginated"})
|
||||
@ApiOperation({ description: 'Get all artists paginated' })
|
||||
@ApiOkResponsePlaginated(_Artist)
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(ArtistController.filterableFields)
|
||||
where: Prisma.ArtistWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Artist>> {
|
||||
@@ -84,15 +101,23 @@ export class ArtistController {
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
include: mapInclude(include, req, ArtistController.includableFields),
|
||||
});
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: "Get an artist by id"})
|
||||
@ApiOkResponse({ type: _Artist})
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
const res = await this.service.get({ id });
|
||||
@ApiOperation({ description: 'Get an artist by id' })
|
||||
@ApiOkResponse({ type: _Artist })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.service.get(
|
||||
{ id },
|
||||
mapInclude(include, req, ArtistController.includableFields),
|
||||
);
|
||||
|
||||
if (res === null) throw new NotFoundException('Artist not found');
|
||||
return res;
|
||||
|
||||
@@ -12,9 +12,13 @@ export class ArtistService {
|
||||
});
|
||||
}
|
||||
|
||||
async get(where: Prisma.ArtistWhereUniqueInput): Promise<Artist | null> {
|
||||
async get(
|
||||
where: Prisma.ArtistWhereUniqueInput,
|
||||
include?: Prisma.ArtistInclude,
|
||||
): Promise<Artist | null> {
|
||||
return this.prisma.artist.findUnique({
|
||||
where,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,14 +28,16 @@ export class ArtistService {
|
||||
cursor?: Prisma.ArtistWhereUniqueInput;
|
||||
where?: Prisma.ArtistWhereInput;
|
||||
orderBy?: Prisma.ArtistOrderByWithRelationInput;
|
||||
include?: Prisma.ArtistInclude;
|
||||
}): Promise<Artist[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { skip, take, cursor, where, orderBy, include } = params;
|
||||
return this.prisma.artist.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,8 @@ import {
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiConflictResponse,
|
||||
ApiCreatedResponse,
|
||||
ApiNoContentResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
@@ -63,11 +60,14 @@ export class AuthController {
|
||||
|
||||
@Get('login/google')
|
||||
@UseGuards(AuthGuard('google'))
|
||||
@ApiOperation({description: 'Redirect to google login page'})
|
||||
@ApiOperation({ description: 'Redirect to google login page' })
|
||||
googleLogin() {}
|
||||
|
||||
@Get('logged/google')
|
||||
@ApiOperation({description: 'Redirect to the front page after connecting to the google account'})
|
||||
@ApiOperation({
|
||||
description:
|
||||
'Redirect to the front page after connecting to the google account',
|
||||
})
|
||||
@UseGuards(AuthGuard('google'))
|
||||
async googleLoginCallbakc(@Req() req: any) {
|
||||
let user = await this.usersService.user({ googleID: req.user.googleID });
|
||||
@@ -79,9 +79,11 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post('register')
|
||||
@ApiOperation({description: 'Register a new user'})
|
||||
@ApiOperation({ description: 'Register a new user' })
|
||||
@ApiConflictResponse({ description: 'Username or email already taken' })
|
||||
@ApiOkResponse({ description: 'Successfully registered, email sent to verify' })
|
||||
@ApiOkResponse({
|
||||
description: 'Successfully registered, email sent to verify',
|
||||
})
|
||||
@ApiBadRequestResponse({ description: 'Invalid data or database error' })
|
||||
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
||||
try {
|
||||
@@ -101,19 +103,21 @@ export class AuthController {
|
||||
@Put('verify')
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({description: 'Verify the email of the user'})
|
||||
@ApiOperation({ description: 'Verify the email of the user' })
|
||||
@ApiOkResponse({ description: 'Successfully verified' })
|
||||
@ApiBadRequestResponse({ description: 'Invalid or expired token' })
|
||||
async verify(@Request() req: any, @Query('token') token: string): Promise<void> {
|
||||
if (await this.authService.verifyMail(req.user.id, token))
|
||||
return;
|
||||
throw new BadRequestException("Invalid token. Expired or invalid.");
|
||||
async verify(
|
||||
@Request() req: any,
|
||||
@Query('token') token: string,
|
||||
): Promise<void> {
|
||||
if (await this.authService.verifyMail(req.user.id, token)) return;
|
||||
throw new BadRequestException('Invalid token. Expired or invalid.');
|
||||
}
|
||||
|
||||
@Put('reverify')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(200)
|
||||
@ApiOperation({description: 'Resend the verification email'})
|
||||
@ApiOperation({ description: 'Resend the verification email' })
|
||||
async reverify(@Request() req: any): Promise<void> {
|
||||
const user = await this.usersService.user({ id: req.user.id });
|
||||
if (!user) throw new BadRequestException('Invalid user');
|
||||
@@ -277,43 +281,29 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully added liked song'})
|
||||
@ApiOkResponse({ description: 'Successfully added liked song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Post('me/likes/:id')
|
||||
addLikedSong(
|
||||
@Request() req: any,
|
||||
@Param('id') songId: number
|
||||
) {
|
||||
return this.usersService.addLikedSong(
|
||||
+req.user.id,
|
||||
+songId,
|
||||
);
|
||||
addLikedSong(@Request() req: any, @Param('id') songId: number) {
|
||||
return this.usersService.addLikedSong(+req.user.id, +songId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully removed liked song'})
|
||||
@ApiOkResponse({ description: 'Successfully removed liked song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Delete('me/likes/:id')
|
||||
removeLikedSong(
|
||||
@Request() req: any,
|
||||
@Param('id') songId: number,
|
||||
) {
|
||||
return this.usersService.removeLikedSong(
|
||||
+req.user.id,
|
||||
+songId,
|
||||
);
|
||||
removeLikedSong(@Request() req: any, @Param('id') songId: number) {
|
||||
return this.usersService.removeLikedSong(+req.user.id, +songId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully retrieved liked song'})
|
||||
@ApiOkResponse({ description: 'Successfully retrieved liked song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me/likes')
|
||||
getLikedSongs(
|
||||
@Request() req: any,
|
||||
) {
|
||||
return this.usersService.getLikedSongs(+req.user.id)
|
||||
getLikedSongs(@Request() req: any) {
|
||||
return this.usersService.getLikedSongs(+req.user.id);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
||||
@@ -79,7 +79,7 @@ export class AuthService {
|
||||
console.log('Password reset token failure', e);
|
||||
return false;
|
||||
}
|
||||
console.log(verified)
|
||||
console.log(verified);
|
||||
await this.userService.updateUser({
|
||||
where: { id: verified.userId },
|
||||
data: { password: new_password },
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { IS_PUBLIC_KEY } from './public';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
console.log(context);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
console.log(isPublic);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
|
||||
4
back/src/auth/public.ts
Normal file
4
back/src/auth/public.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Query,
|
||||
Req,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateGenreDto } from './dto/create-genre.dto';
|
||||
@@ -23,11 +24,18 @@ import { ApiTags } from '@nestjs/swagger';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { Public } from 'src/auth/public';
|
||||
|
||||
@Controller('genre')
|
||||
@ApiTags('genre')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class GenreController {
|
||||
static filterableFields: string[] = ['+id', 'name'];
|
||||
static includableFields: IncludeMap<Prisma.GenreInclude> = {
|
||||
Song: true,
|
||||
};
|
||||
|
||||
constructor(private readonly service: GenreService) {}
|
||||
|
||||
@@ -50,6 +58,7 @@ export class GenreController {
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
const genre = await this.service.get({ id });
|
||||
if (!genre) throw new NotFoundException('Genre not found');
|
||||
@@ -71,6 +80,7 @@ export class GenreController {
|
||||
@Req() req: Request,
|
||||
@FilterQuery(GenreController.filterableFields)
|
||||
where: Prisma.GenreWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Genre>> {
|
||||
@@ -78,13 +88,21 @@ export class GenreController {
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
include: mapInclude(include, req, GenreController.includableFields),
|
||||
});
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
const res = await this.service.get({ id });
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.service.get(
|
||||
{ id },
|
||||
mapInclude(include, req, GenreController.includableFields),
|
||||
);
|
||||
|
||||
if (res === null) throw new NotFoundException('Genre not found');
|
||||
return res;
|
||||
|
||||
@@ -12,9 +12,13 @@ export class GenreService {
|
||||
});
|
||||
}
|
||||
|
||||
async get(where: Prisma.GenreWhereUniqueInput): Promise<Genre | null> {
|
||||
async get(
|
||||
where: Prisma.GenreWhereUniqueInput,
|
||||
include?: Prisma.GenreInclude,
|
||||
): Promise<Genre | null> {
|
||||
return this.prisma.genre.findUnique({
|
||||
where,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,14 +28,16 @@ export class GenreService {
|
||||
cursor?: Prisma.GenreWhereUniqueInput;
|
||||
where?: Prisma.GenreWhereInput;
|
||||
orderBy?: Prisma.GenreOrderByWithRelationInput;
|
||||
include?: Prisma.GenreInclude;
|
||||
}): Promise<Genre[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { skip, take, cursor, where, orderBy, include } = params;
|
||||
return this.prisma.genre.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,20 @@ import {
|
||||
Request,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { SearchHistory, SongHistory } from '@prisma/client';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { SongHistoryDto } from './dto/SongHistoryDto';
|
||||
import { HistoryService } from './history.service';
|
||||
import { SearchHistoryDto } from './dto/SearchHistoryDto';
|
||||
import { SongHistory as _SongHistory } from 'src/_gen/prisma-class/song_history';
|
||||
import { SearchHistory as _SearchHistory} from 'src/_gen/prisma-class/search_history';
|
||||
import { SearchHistory as _SearchHistory } from 'src/_gen/prisma-class/search_history';
|
||||
|
||||
@Controller('history')
|
||||
@ApiTags('history')
|
||||
@@ -26,9 +32,9 @@ export class HistoryController {
|
||||
|
||||
@Get()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: "Get song history of connected user"})
|
||||
@ApiOperation({ description: 'Get song history of connected user' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _SongHistory, isArray: true})
|
||||
@ApiOkResponse({ type: _SongHistory, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
async getHistory(
|
||||
@Request() req: any,
|
||||
@@ -40,9 +46,9 @@ export class HistoryController {
|
||||
|
||||
@Get('search')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: "Get search history of connected user"})
|
||||
@ApiOperation({ description: 'Get search history of connected user' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _SearchHistory, isArray: true})
|
||||
@ApiOkResponse({ type: _SearchHistory, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
async getSearchHistory(
|
||||
@Request() req: any,
|
||||
@@ -54,15 +60,15 @@ export class HistoryController {
|
||||
|
||||
@Post()
|
||||
@HttpCode(201)
|
||||
@ApiOperation({ description: "Create a record of a song played by a user"})
|
||||
@ApiCreatedResponse({ description: "Succesfully created a record"})
|
||||
@ApiOperation({ description: 'Create a record of a song played by a user' })
|
||||
@ApiCreatedResponse({ description: 'Succesfully created a record' })
|
||||
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
|
||||
return this.historyService.createSongHistoryRecord(record);
|
||||
}
|
||||
|
||||
@Post('search')
|
||||
@HttpCode(201)
|
||||
@ApiOperation({ description: "Creates a search record in the users history"})
|
||||
@ApiOperation({ description: 'Creates a search record in the users history' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
async createSearchHistory(
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Get,
|
||||
Query,
|
||||
Req,
|
||||
Request,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
DefaultValuePipe,
|
||||
@@ -12,13 +11,17 @@ import {
|
||||
Body,
|
||||
Delete,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { LessonService } from './lesson.service';
|
||||
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import { Prisma, Skill } from '@prisma/client';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Lesson as _Lesson} from 'src/_gen/prisma-class/lesson';
|
||||
import { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { Request } from 'express';
|
||||
|
||||
export class Lesson {
|
||||
@ApiProperty()
|
||||
@@ -35,6 +38,7 @@ export class Lesson {
|
||||
|
||||
@ApiTags('lessons')
|
||||
@Controller('lesson')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class LessonController {
|
||||
static filterableFields: string[] = [
|
||||
'+id',
|
||||
@@ -42,6 +46,9 @@ export class LessonController {
|
||||
'+requiredLevel',
|
||||
'mainSkill',
|
||||
];
|
||||
static includableFields: IncludeMap<Prisma.LessonInclude> = {
|
||||
LessonHistory: true,
|
||||
};
|
||||
|
||||
constructor(private lessonService: LessonService) {}
|
||||
|
||||
@@ -54,6 +61,7 @@ export class LessonController {
|
||||
@Req() request: Request,
|
||||
@FilterQuery(LessonController.filterableFields)
|
||||
where: Prisma.LessonWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Lesson>> {
|
||||
@@ -61,6 +69,7 @@ export class LessonController {
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
include: mapInclude(include, request, LessonController.includableFields),
|
||||
});
|
||||
return new Plage(ret, request);
|
||||
}
|
||||
@@ -69,8 +78,15 @@ export class LessonController {
|
||||
summary: 'Get a particular lessons',
|
||||
})
|
||||
@Get(':id')
|
||||
async get(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
|
||||
const ret = await this.lessonService.get(id);
|
||||
async get(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
): Promise<Lesson> {
|
||||
const ret = await this.lessonService.get(
|
||||
id,
|
||||
mapInclude(include, req, LessonController.includableFields),
|
||||
);
|
||||
if (!ret) throw new NotFoundException();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -12,22 +12,28 @@ export class LessonService {
|
||||
cursor?: Prisma.LessonWhereUniqueInput;
|
||||
where?: Prisma.LessonWhereInput;
|
||||
orderBy?: Prisma.LessonOrderByWithRelationInput;
|
||||
include?: Prisma.LessonInclude;
|
||||
}): Promise<Lesson[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { skip, take, cursor, where, orderBy, include } = params;
|
||||
return this.prisma.lesson.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
async get(id: number): Promise<Lesson | null> {
|
||||
async get(
|
||||
id: number,
|
||||
include?: Prisma.LessonInclude,
|
||||
): Promise<Lesson | null> {
|
||||
return this.prisma.lesson.findFirst({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,28 @@
|
||||
*/
|
||||
|
||||
import { Type, applyDecorators } from '@nestjs/common';
|
||||
import { ApiExtraModels, ApiOkResponse, ApiProperty, getSchemaPath } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOkResponse,
|
||||
ApiProperty,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
export class PlageMetadata {
|
||||
@ApiProperty()
|
||||
this: string;
|
||||
@ApiProperty({ type: "string", nullable: true, description: "null if there is no next page, couldn't set it in swagger"})
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: "null if there is no next page, couldn't set it in swagger",
|
||||
})
|
||||
next: string | null;
|
||||
@ApiProperty({ type: "string", nullable: true, description: "null if there is no previous page, couldn't set it in swagger" })
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description:
|
||||
"null if there is no previous page, couldn't set it in swagger",
|
||||
})
|
||||
previous: string | null;
|
||||
}
|
||||
|
||||
@@ -55,22 +69,24 @@ export class Plage<T extends object> {
|
||||
}
|
||||
}
|
||||
|
||||
export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(dataDto: DataDto) =>
|
||||
applyDecorators(
|
||||
ApiExtraModels(Plage, dataDto),
|
||||
ApiOkResponse({
|
||||
schema: {
|
||||
allOf: [
|
||||
{ $ref: getSchemaPath(Plage) },
|
||||
{
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
items: { $ref: getSchemaPath(dataDto) },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
)
|
||||
export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(
|
||||
dataDto: DataDto,
|
||||
) =>
|
||||
applyDecorators(
|
||||
ApiExtraModels(Plage, dataDto),
|
||||
ApiOkResponse({
|
||||
schema: {
|
||||
allOf: [
|
||||
{ $ref: getSchemaPath(Plage) },
|
||||
{
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
items: { $ref: getSchemaPath(dataDto) },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiOperation, ApiParam, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { Artist, Genre, Song } from '@prisma/client';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { SearchSongDto } from './dto/search-song.dto';
|
||||
import { SearchService } from './search.service';
|
||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
||||
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
|
||||
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
|
||||
import { mapInclude } from 'src/utils/include';
|
||||
import { SongController } from 'src/song/song.controller';
|
||||
import { GenreController } from 'src/genre/genre.controller';
|
||||
import { ArtistController } from 'src/artist/artist.controller';
|
||||
|
||||
@ApiTags('search')
|
||||
@Controller('search')
|
||||
@@ -27,16 +31,21 @@ export class SearchController {
|
||||
constructor(private readonly searchService: SearchService) {}
|
||||
|
||||
@Get('songs/:query')
|
||||
@ApiOkResponse({ type: _Song, isArray: true})
|
||||
@ApiOperation({ description: "Search a song"})
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token"})
|
||||
@ApiOkResponse({ type: _Song, isArray: true })
|
||||
@ApiOperation({ description: 'Search a song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async searchSong(
|
||||
@Request() req: any,
|
||||
@Query('include') include: string,
|
||||
@Param('query') query: string,
|
||||
): Promise<Song[] | null> {
|
||||
try {
|
||||
const ret = await this.searchService.songByGuess(query, req.user?.id);
|
||||
const ret = await this.searchService.songByGuess(
|
||||
query,
|
||||
req.user?.id,
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
);
|
||||
if (!ret.length) throw new NotFoundException();
|
||||
else return ret;
|
||||
} catch (error) {
|
||||
@@ -46,12 +55,20 @@ export class SearchController {
|
||||
|
||||
@Get('genres/:query')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token"})
|
||||
@ApiOkResponse({ type: _Genre, isArray: true})
|
||||
@ApiOperation({ description: "Search a genre"})
|
||||
async searchGenre(@Request() req: any, @Param('query') query: string): Promise<Genre[] | null> {
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiOkResponse({ type: _Genre, isArray: true })
|
||||
@ApiOperation({ description: 'Search a genre' })
|
||||
async searchGenre(
|
||||
@Request() req: any,
|
||||
@Query('include') include: string,
|
||||
@Param('query') query: string,
|
||||
): Promise<Genre[] | null> {
|
||||
try {
|
||||
const ret = await this.searchService.genreByGuess(query, req.user?.id);
|
||||
const ret = await this.searchService.genreByGuess(
|
||||
query,
|
||||
req.user?.id,
|
||||
mapInclude(include, req, GenreController.includableFields),
|
||||
);
|
||||
if (!ret.length) throw new NotFoundException();
|
||||
else return ret;
|
||||
} catch (error) {
|
||||
@@ -61,12 +78,20 @@ export class SearchController {
|
||||
|
||||
@Get('artists/:query')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _Artist, isArray: true})
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token"})
|
||||
@ApiOperation({ description: "Search an artist"})
|
||||
async searchArtists(@Request() req: any, @Param('query') query: string): Promise<Artist[] | null> {
|
||||
@ApiOkResponse({ type: _Artist, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiOperation({ description: 'Search an artist' })
|
||||
async searchArtists(
|
||||
@Request() req: any,
|
||||
@Query('include') include: string,
|
||||
@Param('query') query: string,
|
||||
): Promise<Artist[] | null> {
|
||||
try {
|
||||
const ret = await this.searchService.artistByGuess(query, req.user?.id);
|
||||
const ret = await this.searchService.artistByGuess(
|
||||
query,
|
||||
req.user?.id,
|
||||
mapInclude(include, req, ArtistController.includableFields),
|
||||
);
|
||||
if (!ret.length) throw new NotFoundException();
|
||||
else return ret;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Album, Artist, Prisma, Song, Genre } from '@prisma/client';
|
||||
import { Artist, Prisma, Song, Genre } from '@prisma/client';
|
||||
import { HistoryService } from 'src/history/history.service';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
|
||||
@@ -10,27 +10,42 @@ export class SearchService {
|
||||
private history: HistoryService,
|
||||
) {}
|
||||
|
||||
async songByGuess(query: string, userID: number): Promise<Song[]> {
|
||||
async songByGuess(
|
||||
query: string,
|
||||
userID: number,
|
||||
include?: Prisma.SongInclude,
|
||||
): Promise<Song[]> {
|
||||
return this.prisma.song.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
},
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
async genreByGuess(query: string, userID: number): Promise<Genre[]> {
|
||||
async genreByGuess(
|
||||
query: string,
|
||||
userID: number,
|
||||
include?: Prisma.GenreInclude,
|
||||
): Promise<Genre[]> {
|
||||
return this.prisma.genre.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
},
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
async artistByGuess(query: string, userID: number): Promise<Artist[]> {
|
||||
async artistByGuess(
|
||||
query: string,
|
||||
userID: number,
|
||||
include?: Prisma.ArtistInclude,
|
||||
): Promise<Artist[]> {
|
||||
return this.prisma.artist.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
},
|
||||
include,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,23 +22,32 @@ import { SongService } from './song.service';
|
||||
import { Request } from 'express';
|
||||
import { Prisma, Song } from '@prisma/client';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiProperty, ApiResponse, ApiResponseProperty, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiProperty,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { HistoryService } from 'src/history/history.service';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
||||
import { SongHistory } from 'src/_gen/prisma-class/song_history';
|
||||
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { Public } from 'src/auth/public';
|
||||
|
||||
class SongHistoryResult {
|
||||
@ApiProperty()
|
||||
best: number;
|
||||
@ApiProperty({ type: SongHistory, isArray: true})
|
||||
@ApiProperty({ type: SongHistory, isArray: true })
|
||||
history: SongHistory[];
|
||||
}
|
||||
|
||||
@Controller('song')
|
||||
@ApiTags('song')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class SongController {
|
||||
static filterableFields: string[] = [
|
||||
'+id',
|
||||
@@ -47,6 +56,13 @@ export class SongController {
|
||||
'+albumId',
|
||||
'+genreId',
|
||||
];
|
||||
static includableFields: IncludeMap<Prisma.SongInclude> = {
|
||||
artist: true,
|
||||
album: true,
|
||||
genre: true,
|
||||
SongHistory: ({ user }) => ({ where: { userID: user.id } }),
|
||||
likedByUsers: ({ user }) => ({ where: { userId: user.id } }),
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly songService: SongService,
|
||||
@@ -54,9 +70,9 @@ export class SongController {
|
||||
) {}
|
||||
|
||||
@Get(':id/midi')
|
||||
@ApiOperation({ description: "Streams the midi file of the requested song"})
|
||||
@ApiNotFoundResponse({ description: "Song not found"})
|
||||
@ApiOkResponse({ description: "Returns the midi file succesfully"})
|
||||
@ApiOperation({ description: 'Streams the midi file of the requested song' })
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the midi file succesfully' })
|
||||
async getMidi(@Param('id', ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException('Song not found');
|
||||
@@ -70,9 +86,12 @@ export class SongController {
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@ApiOperation({ description: "Streams the illustration of the requested song"})
|
||||
@ApiNotFoundResponse({ description: "Song not found"})
|
||||
@ApiOkResponse({ description: "Returns the illustration succesfully"})
|
||||
@ApiOperation({
|
||||
description: 'Streams the illustration of the requested song',
|
||||
})
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the illustration succesfully' })
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException('Song not found');
|
||||
@@ -90,9 +109,11 @@ export class SongController {
|
||||
}
|
||||
|
||||
@Get(':id/musicXml')
|
||||
@ApiOperation({ description: "Streams the musicXML file of the requested song"})
|
||||
@ApiNotFoundResponse({ description: "Song not found"})
|
||||
@ApiOkResponse({ description: "Returns the musicXML file succesfully"})
|
||||
@ApiOperation({
|
||||
description: 'Streams the musicXML file of the requested song',
|
||||
})
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the musicXML file succesfully' })
|
||||
async getMusicXml(@Param('id', ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException('Song not found');
|
||||
@@ -102,7 +123,10 @@ export class SongController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({description: "register a new song in the database, should not be used by the frontend"})
|
||||
@ApiOperation({
|
||||
description:
|
||||
'register a new song in the database, should not be used by the frontend',
|
||||
})
|
||||
async create(@Body() createSongDto: CreateSongDto) {
|
||||
try {
|
||||
return await this.songService.createSong({
|
||||
@@ -118,7 +142,6 @@ export class SongController {
|
||||
: undefined,
|
||||
});
|
||||
} catch {
|
||||
|
||||
throw new ConflictException(
|
||||
await this.songService.song({ name: createSongDto.name }),
|
||||
);
|
||||
@@ -126,7 +149,7 @@ export class SongController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: "delete a song by id"})
|
||||
@ApiOperation({ description: 'delete a song by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.songService.deleteSong({ id });
|
||||
@@ -140,6 +163,7 @@ export class SongController {
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Song>> {
|
||||
@@ -147,16 +171,26 @@ export class SongController {
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
include: mapInclude(include, req, SongController.includableFields),
|
||||
});
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: "Get a specific song data"})
|
||||
@ApiNotFoundResponse({ description: "Song not found"})
|
||||
@ApiOkResponse({ type: _Song, description: "Requested song"})
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
const res = await this.songService.song({ id });
|
||||
@ApiOperation({ description: 'Get a specific song data' })
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ type: _Song, description: 'Requested song' })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query('include') include: string,
|
||||
) {
|
||||
const res = await this.songService.song(
|
||||
{
|
||||
id,
|
||||
},
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
);
|
||||
|
||||
if (res === null) throw new NotFoundException('Song not found');
|
||||
return res;
|
||||
@@ -164,9 +198,13 @@ export class SongController {
|
||||
|
||||
@Get(':id/history')
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ description: "get the history of the connected user on a specific song"})
|
||||
@ApiOkResponse({ type: SongHistoryResult, description: "Records of previous games of the user"})
|
||||
@ApiOperation({
|
||||
description: 'get the history of the connected user on a specific song',
|
||||
})
|
||||
@ApiOkResponse({
|
||||
type: SongHistoryResult,
|
||||
description: 'Records of previous games of the user',
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
||||
return this.historyService.getForSong({
|
||||
|
||||
@@ -9,7 +9,7 @@ export class SongService {
|
||||
async songByArtist(data: number): Promise<Song[]> {
|
||||
return this.prisma.song.findMany({
|
||||
where: {
|
||||
artistId: {equals: data},
|
||||
artistId: { equals: data },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -22,9 +22,11 @@ export class SongService {
|
||||
|
||||
async song(
|
||||
songWhereUniqueInput: Prisma.SongWhereUniqueInput,
|
||||
include?: Prisma.SongInclude,
|
||||
): Promise<Song | null> {
|
||||
return this.prisma.song.findUnique({
|
||||
where: songWhereUniqueInput,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,14 +36,16 @@ export class SongService {
|
||||
cursor?: Prisma.SongWhereUniqueInput;
|
||||
where?: Prisma.SongWhereInput;
|
||||
orderBy?: Prisma.SongOrderByWithRelationInput;
|
||||
include?: Prisma.SongInclude;
|
||||
}): Promise<Song[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { skip, take, cursor, where, orderBy, include } = params;
|
||||
return this.prisma.song.findMany({
|
||||
skip,
|
||||
take,
|
||||
cursor,
|
||||
where,
|
||||
orderBy,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Controller, Get, Post, Param, NotFoundException, Response } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
NotFoundException,
|
||||
Response,
|
||||
} from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { User } from 'src/models/user';
|
||||
@@ -22,7 +29,9 @@ export class UsersController {
|
||||
}
|
||||
|
||||
@Get(':id/picture')
|
||||
@ApiOkResponse({description: 'Return the profile picture of the requested user'})
|
||||
@ApiOkResponse({
|
||||
description: 'Return the profile picture of the requested user',
|
||||
})
|
||||
async getPicture(@Response() res: any, @Param('id') id: number) {
|
||||
return await this.usersService.getProfilePicture(+id, res);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,7 @@ import fetch from 'node-fetch';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async user(
|
||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
|
||||
@@ -101,36 +99,22 @@ export class UsersService {
|
||||
resp.body!.pipe(res);
|
||||
}
|
||||
|
||||
async addLikedSong(
|
||||
userId: number,
|
||||
songId: number,
|
||||
) {
|
||||
return this.prisma.likedSongs.create(
|
||||
{
|
||||
data: { songId: songId, userId: userId }
|
||||
}
|
||||
)
|
||||
async addLikedSong(userId: number, songId: number) {
|
||||
return this.prisma.likedSongs.create({
|
||||
data: { songId: songId, userId: userId },
|
||||
});
|
||||
}
|
||||
|
||||
async getLikedSongs(
|
||||
userId: number,
|
||||
) {
|
||||
return this.prisma.likedSongs.findMany(
|
||||
{
|
||||
where: { userId: userId },
|
||||
}
|
||||
)
|
||||
async getLikedSongs(userId: number) {
|
||||
return this.prisma.likedSongs.findMany({
|
||||
where: { userId: userId },
|
||||
});
|
||||
}
|
||||
|
||||
async removeLikedSong(
|
||||
userId: number,
|
||||
songId: number,
|
||||
) {
|
||||
return this.prisma.likedSongs.deleteMany(
|
||||
{
|
||||
where: { userId: userId, songId: songId },
|
||||
}
|
||||
)
|
||||
async removeLikedSong(userId: number, songId: number) {
|
||||
return this.prisma.likedSongs.deleteMany({
|
||||
where: { userId: userId, songId: songId },
|
||||
});
|
||||
}
|
||||
|
||||
async addScore(
|
||||
|
||||
33
back/src/utils/include.ts
Normal file
33
back/src/utils/include.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Request } from 'express';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
export type IncludeMap<IncludeType> = {
|
||||
[key in keyof IncludeType]:
|
||||
| boolean
|
||||
| ((ctx: { user: { id: number; username: string } }) => IncludeType[key]);
|
||||
};
|
||||
|
||||
export function mapInclude<IncludeType>(
|
||||
include: string | undefined,
|
||||
req: Request,
|
||||
fields: IncludeMap<IncludeType>,
|
||||
): IncludeType | undefined {
|
||||
if (!include) return undefined;
|
||||
|
||||
const ret: IncludeType = {} as IncludeType;
|
||||
for (const key of include.split(',')) {
|
||||
const value =
|
||||
typeof fields[key] === 'function'
|
||||
? fields[key]({ user: req.user })
|
||||
: fields[key];
|
||||
if (value !== false && value !== undefined) ret[key] = value;
|
||||
else {
|
||||
throw new BadRequestException(
|
||||
`Invalid include, ${key} is not valid. Valid includes are: ${Object.keys(
|
||||
fields,
|
||||
).join(', ')}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ Documentation Tests of the /song route.
|
||||
... Ensures that the songs CRUD works corectly.
|
||||
|
||||
Resource ../rest.resource
|
||||
Resource ../auth/auth.resource
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
@@ -133,5 +134,47 @@ Get midi file
|
||||
Integer response status 201
|
||||
GET /song/${res.body.id}/midi
|
||||
Integer response status 200
|
||||
#Output
|
||||
# Output
|
||||
[Teardown] DELETE /song/${res.body.id}
|
||||
|
||||
Find a song with artist
|
||||
[Documentation] Create a song and find it with it's artist
|
||||
&{res2}= POST /artist { "name": "Tghjmk"}
|
||||
Output
|
||||
Integer response status 201
|
||||
&{res}= POST
|
||||
... /song
|
||||
... {"name": "Mama miaeyi", "artistId": ${res2.body.id}, "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"}
|
||||
Output
|
||||
Integer response status 201
|
||||
&{get}= GET /song/${res.body.id}?include=artist
|
||||
Output
|
||||
Integer response status 200
|
||||
Should Be Equal ${res2.body} ${get.body.artist}
|
||||
[Teardown] Run Keywords DELETE /song/${res.body.id}
|
||||
... AND DELETE /artist/${res2.body.id}
|
||||
|
||||
Find a song with artist and history
|
||||
[Documentation] Create a song and find it with it's artist
|
||||
${userID}= RegisterLogin wowusersfkj
|
||||
&{res2}= POST /artist { "name": "Tghjmk"}
|
||||
Output
|
||||
Integer response status 201
|
||||
&{res}= POST
|
||||
... /song
|
||||
... {"name": "Mama miaeyi", "artistId": ${res2.body.id}, "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"}
|
||||
Output
|
||||
Integer response status 201
|
||||
&{res3}= POST
|
||||
... /history
|
||||
... { "songID": ${res.body.id}, "userID": ${userID}, "score": 12, "difficulties": {}, "info": {} }
|
||||
Output
|
||||
Integer response status 201
|
||||
&{get}= GET /song/${res.body.id}?include=artist,SongHistory
|
||||
Output
|
||||
Integer response status 200
|
||||
Should Be Equal ${res2.body} ${get.body.artist}
|
||||
Should Be Equal ${res3.body} ${get.body.SongHistory[0]}
|
||||
[Teardown] Run Keywords DELETE /auth/me
|
||||
... AND DELETE /song/${res.body.id}
|
||||
... AND DELETE /artist/${res2.body.id}
|
||||
|
||||
Reference in New Issue
Block a user