From 6bb83a3d5c6a0da52bfdd07b055c9e7e33f393af Mon Sep 17 00:00:00 2001 From: legonzaur Date: Tue, 31 Dec 2024 15:47:36 +0100 Subject: [PATCH] feat : add concept module --- src/app.module.ts | 2 + src/concept/concept.controller.ts | 104 ++++++++++++++++++ src/concept/concept.module.ts | 4 + src/concept/concept.pipe.ts | 20 ++++ src/concept/concept.service.ts | 10 +- src/concept/dto/conceptrecieve.dto.ts | 7 ++ src/games/controllers/games.controller.ts | 38 +++---- src/games/pipe/game.pipe.ts | 6 +- src/word/entities/topic.entity.ts | 12 +- .../{express-session.d.ts => express.d.ts} | 0 10 files changed, 162 insertions(+), 41 deletions(-) create mode 100644 src/concept/concept.controller.ts create mode 100644 src/concept/concept.pipe.ts create mode 100644 src/concept/dto/conceptrecieve.dto.ts rename types.d.ts/{express-session.d.ts => express.d.ts} (100%) diff --git a/src/app.module.ts b/src/app.module.ts index e769b4e..b0540e3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,6 +15,7 @@ import { WordModule } from './word/words.module'; import { Message } from './games/entities/message.entity'; import { LobbyModule } from './lobby/lobby.module'; import { Concept } from './concept/entities/concept.entity'; +import { ConceptModule } from './concept/concept.module'; const configService = new ConfigService(); @@ -48,6 +49,7 @@ const configService = new ConfigService(); GamesModule, UsersModule, WordModule, + ConceptModule, ], }) export class AppModule {} diff --git a/src/concept/concept.controller.ts b/src/concept/concept.controller.ts new file mode 100644 index 0000000..70bd353 --- /dev/null +++ b/src/concept/concept.controller.ts @@ -0,0 +1,104 @@ +import { + Body, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + Param, + ParseIntPipe, + Post, + Put, + Sse, + UseGuards, +} from '@nestjs/common'; + +import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { AuthGuard } from 'src/users/guards/auth.guard'; + +import { fromEvent, map, Observable } from 'rxjs'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +import { ConceptService } from './concept.service'; +import { Concept } from './entities/concept.entity'; +import { ConceptPipe } from './concept.pipe'; +import { ConceptRecieveDTO } from './dto/conceptrecieve.dto'; +import { ConceptResponseDTO } from 'events/dto/conceptresponse.dto'; + +@ApiTags('concepts') +@Controller('concepts') +export class ConceptController { + constructor( + private readonly conceptService: ConceptService, + private eventEmitter: EventEmitter2, + ) {} + + @Sse('/subscribe') + subscribe(): Observable<{ data: string }> { + return fromEvent(this.eventEmitter, this.conceptService.sse_prefix).pipe( + map((payload) => { + return { + data: JSON.stringify(payload), + }; + }), + ); + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a concept' }) + @UseGuards(AuthGuard) + async delete(@Param('id') id: number) { + const concept = await this.conceptService.getById(id); + if (!concept) { + throw new HttpException('Concept not found', HttpStatus.NOT_FOUND); + } + await this.conceptService.delete(concept); + } + + @Post('/') + @ApiOperation({ summary: 'Submit a concept' }) + @ApiBody({ type: ConceptRecieveDTO }) + @UseGuards(AuthGuard) + async create(@Body('value') value: string) { + const concept = await this.conceptService.create(value, 'placeholder.jpg'); + return new ConceptResponseDTO(concept); + } + + @Get('/') + @ApiOperation({ summary: 'List enabled concept' }) + async read() { + return (await this.conceptService.listActive()).map( + (w) => new ConceptResponseDTO(w), + ); + } + + @Get('/all') + @ApiOperation({ summary: 'List all concepts' }) + async readAll() { + return (await this.conceptService.list()).map( + (w) => new ConceptResponseDTO(w), + ); + } + + @Put(':id/enable') + @ApiOperation({ summary: 'Enable a concept' }) + @UseGuards(AuthGuard) + async enable( + @Param('id', ParseIntPipe) _id: string, + @Param('id', ConceptPipe) concept: Concept, + ) { + await this.conceptService.enable(concept); + return new ConceptResponseDTO(concept); + } + + @Put(':id/disable') + @ApiOperation({ summary: 'Disable a word' }) + @UseGuards(AuthGuard) + async disable( + @Param('id', ParseIntPipe) _id: string, + @Param('id', ConceptPipe) concept: Concept, + ) { + await this.conceptService.disable(concept); + return new ConceptResponseDTO(concept); + } +} diff --git a/src/concept/concept.module.ts b/src/concept/concept.module.ts index 2b9f2be..96985d1 100644 --- a/src/concept/concept.module.ts +++ b/src/concept/concept.module.ts @@ -4,8 +4,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersModule } from 'src/users/users.module'; import { Concept } from './entities/concept.entity'; +import { ConceptService } from './concept.service'; +import { ConceptController } from './concept.controller'; @Module({ imports: [UsersModule, TypeOrmModule.forFeature([Concept])], + providers: [ConceptService], + controllers: [ConceptController], }) export class ConceptModule {} diff --git a/src/concept/concept.pipe.ts b/src/concept/concept.pipe.ts new file mode 100644 index 0000000..6c11b84 --- /dev/null +++ b/src/concept/concept.pipe.ts @@ -0,0 +1,20 @@ +import { + HttpException, + HttpStatus, + Injectable, + PipeTransform, +} from '@nestjs/common'; +import { ConceptService } from './concept.service'; + +@Injectable() +export class ConceptPipe implements PipeTransform { + constructor(private readonly conceptService: ConceptService) {} + + async transform(id: number) { + const word = await this.conceptService.getById(id); + if (!word) { + throw new HttpException('Word not found', HttpStatus.NOT_FOUND); + } + return word; + } +} diff --git a/src/concept/concept.service.ts b/src/concept/concept.service.ts index 8b28e53..8dc9dc8 100644 --- a/src/concept/concept.service.ts +++ b/src/concept/concept.service.ts @@ -12,7 +12,7 @@ import { } from 'events/concept.events'; @Injectable() -export class WordService { +export class ConceptService { sse_prefix = 'sse.concept'; constructor( @InjectRepository(Concept) private conceptRepository: Repository, @@ -42,13 +42,13 @@ export class WordService { } async listActive() { - const words = await this.conceptRepository.findBy({ enabled: true }); - return words; + const concepts = await this.conceptRepository.findBy({ enabled: true }); + return concepts; } async list() { - const words = await this.conceptRepository.find(); - return words; + const concepts = await this.conceptRepository.find(); + return concepts; } async enable(concept: Concept) { diff --git a/src/concept/dto/conceptrecieve.dto.ts b/src/concept/dto/conceptrecieve.dto.ts new file mode 100644 index 0000000..29b88d7 --- /dev/null +++ b/src/concept/dto/conceptrecieve.dto.ts @@ -0,0 +1,7 @@ +export class ConceptRecieveDTO { + constructor(partial: Partial) { + Object.assign(this, partial); + } + + value: string; +} diff --git a/src/games/controllers/games.controller.ts b/src/games/controllers/games.controller.ts index e65cdf6..487eea6 100644 --- a/src/games/controllers/games.controller.ts +++ b/src/games/controllers/games.controller.ts @@ -6,7 +6,6 @@ import { HttpStatus, Param, Post, - Session, Sse, UseGuards, } from '@nestjs/common'; @@ -19,11 +18,12 @@ import { Observable, fromEvent, map } from 'rxjs'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { GameService } from '../services/games.service'; import { ConfigService } from '@nestjs/config'; -import { SessionData } from 'express-session'; // import { WordResponseDTO } from 'events/dto/wordresponse.dto'; import { WordRecieveDTO } from 'src/word/dto/wordrecieve.dto'; import { LobbyService } from 'src/lobby/lobby.service'; import { WordResponseDTO } from 'events/dto/wordresponse.dto'; +import { GetUser } from 'src/users/user.pipe'; +import { User } from 'src/users/entities/user.entity'; @ApiTags('game') @Controller('games/:id') @@ -52,7 +52,7 @@ export class GamesController { @UseGuards(AuthGuard) async start( @Param('id') id: string, - @Session() session: SessionData, + @GetUser() user: User, @Param('id', GamePipe) game: Game, ) { if (game.started) { @@ -64,7 +64,7 @@ export class GamesController { HttpStatus.BAD_REQUEST, ); } - if (game.ownerId != session.user?.id) { + if (game.ownerId != user.id) { throw new HttpException( 'You can only start games you own', HttpStatus.FORBIDDEN, @@ -79,42 +79,39 @@ export class GamesController { @UseGuards(AuthGuard) surrender( @Param('id') id: string, - @Session() session: SessionData, - @Param('id', GamePipe) game: Game, + @GetUser() user: User, + @Param('id', GameStartedPipe) game: Game, ) { - if (!game.started) { - throw new HttpException('Game has not started', HttpStatus.BAD_REQUEST); - } - return this.lobbyService.leaveGame(game, session.user); + return this.lobbyService.leaveGame(game, user); } @Post('kick/:playerId') @ApiOperation({ summary: 'Kick a player' }) @UseGuards(AuthGuard) kick( - @Session() session: SessionData, + @GetUser() user: User, @Param('id') id: string, @Param('id', GamePipe) game: Game, @Param('playerId') playerId: string, ) { - if (session.user.id != game.ownerId) { + if (user.id != game.ownerId) { throw new HttpException( 'You do do not own this game', HttpStatus.FORBIDDEN, ); } - return this.lobbyService.kickPlayer(game, session.user, playerId); + return this.lobbyService.kickPlayer(game, user, playerId); } @Get('currentWord') @ApiOperation({ summary: 'Read Current Word' }) @UseGuards(AuthGuard) getCurrentWord( - @Session() session: SessionData, + @GetUser() user: User, @Param('id') id: string, - @Param('id', GamePipe) game: Game, + @Param('id', GameStartedPipe) game: Game, ) { - if (session.user.id != game.currentPlayer.userId) { + if (user.id != game.currentPlayer.userId) { throw new HttpException( 'It is not your turn to play', HttpStatus.FORBIDDEN, @@ -128,15 +125,12 @@ export class GamesController { @ApiBody({ type: WordRecieveDTO }) @UseGuards(AuthGuard) async postMessage( - @Session() session: SessionData, + @GetUser() user: User, @Param('id') id: string, - @Param('id', GamePipe) game: Game, + @Param('id', GameStartedPipe) game: Game, @Body('value') value: string, ) { - const player = game.players.find((p) => p.userId == session.user.id); - if (!game.started) { - throw new HttpException('Game not started', HttpStatus.BAD_REQUEST); - } + const player = game.players.find((p) => p.userId == user.id); if (!player) { throw new HttpException('You are not in this game', HttpStatus.FORBIDDEN); } diff --git a/src/games/pipe/game.pipe.ts b/src/games/pipe/game.pipe.ts index 1696abb..c92688b 100644 --- a/src/games/pipe/game.pipe.ts +++ b/src/games/pipe/game.pipe.ts @@ -15,7 +15,7 @@ export class GamePipe implements PipeTransform { @InjectRepository(Game) private gameRepository: Repository, ) {} - async transform(value: any) { + async transform(value: number) { const game = await this.gameRepository.findOneBy({ id: value }); if (!game) { throw new HttpException('Game not found', HttpStatus.NOT_FOUND); @@ -30,8 +30,8 @@ export class GameStartedPipe implements PipeTransform { @InjectRepository(Game) private gameRepository: Repository, ) {} - async transform(value: any) { - const game = await this.gameRepository.findOne(value); + async transform(value: number) { + const game = await this.gameRepository.findOneBy({ id: value }); if (!game) { throw new HttpException('Game not found', HttpStatus.NOT_FOUND); } diff --git a/src/word/entities/topic.entity.ts b/src/word/entities/topic.entity.ts index 625b67f..bcf3167 100644 --- a/src/word/entities/topic.entity.ts +++ b/src/word/entities/topic.entity.ts @@ -1,14 +1,4 @@ -import { User } from 'src/users/entities/user.entity'; -import { - Column, - Entity, - JoinTable, - ManyToMany, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - Unique, -} from 'typeorm'; +import { Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Word } from './word.entity'; @Entity() diff --git a/types.d.ts/express-session.d.ts b/types.d.ts/express.d.ts similarity index 100% rename from types.d.ts/express-session.d.ts rename to types.d.ts/express.d.ts