diff --git a/.vscode/settings.json b/.vscode/settings.json index 387ad48..c6fc682 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" - } + }, + "javascript.preferences.quoteStyle": "single", + "typescript.preferences.quoteStyle": "single" } diff --git a/package-lock.json b/package-lock.json index 9b91835..75e1dc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "@types/ranjs": "^1.22.7", + "class-transformer": "^0.5.1", "connect-redis": "^8.0.1", "express-session": "^1.18.0", "pg": "^8.13.1", @@ -4488,6 +4489,12 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", diff --git a/package.json b/package.json index 7023830..39134ee 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "@types/ranjs": "^1.22.7", + "class-transformer": "^0.5.1", "connect-redis": "^8.0.1", "express-session": "^1.18.0", "pg": "^8.13.1", diff --git a/src/games/controllers/lobby.controller.ts b/src/games/controllers/lobby.controller.ts index 66d9d0b..4a4722d 100644 --- a/src/games/controllers/lobby.controller.ts +++ b/src/games/controllers/lobby.controller.ts @@ -17,6 +17,7 @@ import { GamePipe } from '../pipe/game.pipe'; import { Game } from '../entities/game.entity'; import { Observable, fromEvent, map } from 'rxjs'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { GameResponseDTO } from '../dto/gameresponse.dto'; @ApiTags('lobby') @Controller('games') @@ -52,9 +53,9 @@ export class LobbyController { @Get() @ApiOperation({ summary: 'List all games' }) - findAll() { - const games = this.lobbyService.findAll(); - return games; + async findAll(): Promise { + const games = await this.lobbyService.findAll(); + return games.map((g) => new GameResponseDTO(g)); } @Sse('/subscribe') @@ -70,9 +71,11 @@ export class LobbyController { @Get(':id') @ApiOperation({ summary: 'Get game information' }) - async findOne(@Param('id') id: string, @Param('id', GamePipe) game: Game) { - return game; - // return new ReadGameDto(game); + async findOne( + @Param('id') id: string, + @Param('id', GamePipe) game: Game, + ): Promise { + return new GameResponseDTO(game); } @Delete(':id') diff --git a/src/games/dto/conceptinturnresponse.dto.ts b/src/games/dto/conceptinturnresponse.dto.ts new file mode 100644 index 0000000..0b82ac6 --- /dev/null +++ b/src/games/dto/conceptinturnresponse.dto.ts @@ -0,0 +1,20 @@ +import { Expose, Type } from 'class-transformer'; +import { ConceptResponseDTO } from './conceptresponse.dto'; + +export class ConceptInTurnResponseDTO { + constructor(partial: Partial) { + Object.assign(this, partial); + } + @Type(() => ConceptResponseDTO) + @Expose() + concept: ConceptResponseDTO; + + @Expose() + order: number; + + @Expose() + subconcept: number; + + @Expose() + markers: number; +} diff --git a/src/games/dto/conceptresponse.dto.ts b/src/games/dto/conceptresponse.dto.ts new file mode 100644 index 0000000..ed508fb --- /dev/null +++ b/src/games/dto/conceptresponse.dto.ts @@ -0,0 +1,13 @@ +import { Expose } from 'class-transformer'; + +export class ConceptResponseDTO { + constructor(partial: Partial) { + Object.assign(this, partial); + } + + @Expose() + id: number; + + @Expose() + value: string; +} diff --git a/src/games/dto/gameresponse.dto.ts b/src/games/dto/gameresponse.dto.ts new file mode 100644 index 0000000..7fd1840 --- /dev/null +++ b/src/games/dto/gameresponse.dto.ts @@ -0,0 +1,36 @@ +import { Expose, Type } from 'class-transformer'; +import { UserResponseDTO } from 'src/users/dto/userresponse.dto'; +import { PlayerResponseDTO } from './playerresponse.dto'; +import { ConceptInTurnResponseDTO } from './conceptinturnresponse.dto'; + +export class GameResponseDTO { + constructor(partial: Partial) { + Object.assign(this, partial); + this.owner = partial.owner; + this.players = partial.players; + this.currentPlayer = partial.currentPlayer; + this.currentConcepts = partial.currentConcepts; + } + + @Expose() + id: number; + + @Type(() => UserResponseDTO) + @Expose() + owner: UserResponseDTO; + + @Type(() => PlayerResponseDTO) + @Expose() + players: PlayerResponseDTO[]; + + @Type(() => PlayerResponseDTO) + @Expose() + currentPlayer: PlayerResponseDTO; + + @Expose() + started: boolean; + + @Type(() => ConceptInTurnResponseDTO) + @Expose() + currentConcepts: ConceptInTurnResponseDTO[]; +} diff --git a/src/games/dto/playerresponse.dto.ts b/src/games/dto/playerresponse.dto.ts new file mode 100644 index 0000000..cf76f65 --- /dev/null +++ b/src/games/dto/playerresponse.dto.ts @@ -0,0 +1,21 @@ +import { Expose, Transform, Type } from 'class-transformer'; +import { UserResponseDTO } from 'src/users/dto/userresponse.dto'; + +export class PlayerResponseDTO { + constructor(partial: Partial) { + Object.assign(this, partial); + this.user = partial.user; + } + + @Type(() => UserResponseDTO) + @Expose() + user: UserResponseDTO; + + @Expose() + get userId(): string { + return this.user.userId; + } + + @Expose() + score: number; +} diff --git a/src/games/entities/game.entity.ts b/src/games/entities/game.entity.ts index 45d97e7..7582649 100644 --- a/src/games/entities/game.entity.ts +++ b/src/games/entities/game.entity.ts @@ -19,10 +19,10 @@ export class Game { @Column() seed: string; - @ManyToOne(() => User) + @ManyToOne(() => User, { eager: true }) owner: User; - @OneToMany(() => Player, (player) => player.game, { onDelete: 'CASCADE' }) + @OneToMany(() => Player, (player) => player.game, { eager: true }) @JoinTable() players: Player[]; diff --git a/src/games/entities/player.entity.ts b/src/games/entities/player.entity.ts index 2fb262e..23ac1dc 100644 --- a/src/games/entities/player.entity.ts +++ b/src/games/entities/player.entity.ts @@ -17,7 +17,7 @@ export class Player { @PrimaryGeneratedColumn() id: number; - @ManyToOne(() => User) + @ManyToOne(() => User, { eager: true }) user: User; @ManyToOne(() => Game) @@ -25,4 +25,8 @@ export class Player { @Column({ default: 0 }) score: number; + + get userId(): string { + return this.user.userId; + } } diff --git a/src/games/pipe/game.pipe.ts b/src/games/pipe/game.pipe.ts index f235f25..1696abb 100644 --- a/src/games/pipe/game.pipe.ts +++ b/src/games/pipe/game.pipe.ts @@ -16,7 +16,7 @@ export class GamePipe implements PipeTransform { ) {} async transform(value: any) { - const game = await this.gameRepository.findBy({ id: value }); + const game = await this.gameRepository.findOneBy({ id: value }); if (!game) { throw new HttpException('Game not found', HttpStatus.NOT_FOUND); } diff --git a/src/main.ts b/src/main.ts index b7fba73..449bfd7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,8 @@ -import { NestFactory } from '@nestjs/core'; +import { NestFactory, Reflector } from '@nestjs/core'; import * as session from 'express-session'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { ClassSerializerInterceptor, INestApplication } from '@nestjs/common'; import { RedisStore } from 'connect-redis'; import { createClient } from 'redis'; @@ -30,6 +31,13 @@ async function bootstrap() { }), ); + app.useGlobalInterceptors( + new ClassSerializerInterceptor(app.get(Reflector), { + // strategy: 'excludeAll', 👈 we'll talk about this later + excludeExtraneousValues: true, + }), + ); + await app.listen(8765); } bootstrap(); diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts deleted file mode 100644 index be5dbc9..0000000 --- a/src/users/dto/create-user.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class CreateUserDto { - method: 'discord' | 'password'; - username?: string; - password: string; -} diff --git a/src/users/dto/login.dto.ts b/src/users/dto/login.dto.ts deleted file mode 100644 index 125ed45..0000000 --- a/src/users/dto/login.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class LoginDto { - constructor(userId: string, username: string, avatar: string) { - this.userId = userId; - this.username = username; - this.avatar = avatar; - } - @ApiProperty() - userId: string; - - @ApiProperty() - username: string; - - @ApiProperty() - avatar: string; -} diff --git a/src/users/dto/userresponse.dto.ts b/src/users/dto/userresponse.dto.ts new file mode 100644 index 0000000..930d03b --- /dev/null +++ b/src/users/dto/userresponse.dto.ts @@ -0,0 +1,15 @@ +import { Expose } from 'class-transformer'; + +export class UserResponseDTO { + constructor(partial: Partial) { + Object.assign(this, partial); + } + @Expose() + userId: string; + + @Expose() + username: string; + + @Expose() + avatar: string; +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index d624e92..e7f774f 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -8,7 +8,7 @@ import { Redirect, } from '@nestjs/common'; import { UsersService } from './users.service'; -import { LoginDto } from './dto/login.dto'; +import { UserResponseDTO } from './dto/userresponse.dto'; import { ApiTags } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; @@ -39,14 +39,16 @@ export class UsersController { } @Get('/whoami') - async remove(@Session() session: Record): Promise { + async remove( + @Session() session: Record, + ): Promise { if (!session.tokenData) { throw new HttpException( "Session doesn't contains auth information", HttpStatus.UNAUTHORIZED, ); } - const user = await this.usersService.discordLogin(session); - return user; + await this.usersService.discordLogin(session); + return new UserResponseDTO(session.user); } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 8ea48b4..62278e7 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,5 +1,5 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { LoginDto } from './dto/login.dto'; +import { UserResponseDTO } from './dto/userresponse.dto'; import { URLSearchParams } from 'url'; import { User } from './entities/user.entity'; import { InjectRepository } from '@nestjs/typeorm'; @@ -108,7 +108,7 @@ export class UsersService { async discordLogin( session: Record, recursive: boolean = false, - ): Promise { + ): Promise { const req2 = await fetch('https://discord.com/api/users/@me', { headers: { authorization: @@ -135,13 +135,12 @@ export class UsersService { } } const data2 = await req2.json(); - const dto = new LoginDto(data2.id, data2.global_name, data2.avatar); - const user = await this.getOrCreateUser(dto.userId, dto); + const user = await this.getOrCreateUser(data2.id, data2); session.user = user; - return dto; + return user; } - async getOrCreateUser(userId: string, data?: LoginDto) { + async getOrCreateUser(userId: string, data?: UserResponseDTO) { let user = await this.usersRepository.findOneBy({ userId }); if (!user) { user = this.usersRepository.create({ ...data, userId });