prisma
This commit is contained in:
parent
4dee8677df
commit
0fb6e53d7a
4 changed files with 10041 additions and 57 deletions
|
|
@ -4,4 +4,5 @@ alwaysApply: true
|
||||||
App runs on a dedicated server with coolify and docker.
|
App runs on a dedicated server with coolify and docker.
|
||||||
Migrations are run via docker on the server.
|
Migrations are run via docker on the server.
|
||||||
Keep code clean, never add todos and stubs outside tests.
|
Keep code clean, never add todos and stubs outside tests.
|
||||||
|
Use strict static types, never add 'any' type
|
||||||
Keep the final review short.
|
Keep the final review short.
|
||||||
9886
backend/package-lock.json
generated
Normal file
9886
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -34,12 +34,12 @@
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"pg": "^8.13.1",
|
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
"pg": "^8.13.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"socket.io": "^4.8.3"
|
"socket.io": "^4.8.3"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { RoomsService } from '../rooms/rooms.service';
|
||||||
import { RoomEventsService } from './room-events.service';
|
import { RoomEventsService } from './room-events.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { RoomPackService } from '../room-pack/room-pack.service';
|
import { RoomPackService } from '../room-pack/room-pack.service';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
interface PlayerAction {
|
interface PlayerAction {
|
||||||
action: 'revealAnswer' | 'nextQuestion' | 'prevQuestion';
|
action: 'revealAnswer' | 'nextQuestion' | 'prevQuestion';
|
||||||
|
|
@ -23,6 +24,30 @@ interface PlayerAction {
|
||||||
answerId?: string; // UUID ответа
|
answerId?: string; // UUID ответа
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Question {
|
||||||
|
id: string;
|
||||||
|
text?: string;
|
||||||
|
question?: string;
|
||||||
|
answers: Array<{
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
points: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomWithPack = Prisma.RoomGetPayload<{
|
||||||
|
include: {
|
||||||
|
roomPack: true;
|
||||||
|
participants: true;
|
||||||
|
}
|
||||||
|
}> & {
|
||||||
|
currentQuestionId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RevealedAnswers {
|
||||||
|
[questionId: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
cors: {
|
cors: {
|
||||||
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||||
|
|
@ -85,24 +110,30 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
await this.roomsService.updateRoomStatus(payload.roomId, 'PLAYING');
|
await this.roomsService.updateRoomStatus(payload.roomId, 'PLAYING');
|
||||||
|
|
||||||
// Инициализировать первый вопрос и игрока
|
// Инициализировать первый вопрос и игрока
|
||||||
const room = await this.prisma.room.findUnique({
|
const room = (await this.prisma.room.findUnique({
|
||||||
where: { id: payload.roomId },
|
where: { id: payload.roomId },
|
||||||
include: {
|
include: {
|
||||||
roomPack: true,
|
roomPack: true,
|
||||||
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
||||||
}
|
} as Prisma.RoomInclude,
|
||||||
});
|
})) as unknown as RoomWithPack | null;
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
const questions = room.roomPack?.questions as any[] || [];
|
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||||
const firstQuestion = questions[0];
|
const firstQuestion = questions[0];
|
||||||
const firstParticipant = room.participants[0];
|
const firstParticipant = room.participants[0];
|
||||||
|
|
||||||
if (firstQuestion && firstParticipant) {
|
// Убеждаемся что firstQuestion.id - строка (UUID)
|
||||||
|
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
|
||||||
|
? firstQuestion.id
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (firstQuestionId && firstParticipant) {
|
||||||
await this.prisma.room.update({
|
await this.prisma.room.update({
|
||||||
where: { id: payload.roomId },
|
where: { id: payload.roomId },
|
||||||
data: {
|
data: {
|
||||||
currentQuestionId: firstQuestion.id,
|
currentQuestionId: firstQuestionId,
|
||||||
|
currentQuestionIndex: 0,
|
||||||
currentPlayerId: firstParticipant.id,
|
currentPlayerId: firstParticipant.id,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -115,13 +146,13 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
@SubscribeMessage('playerAction')
|
@SubscribeMessage('playerAction')
|
||||||
async handlePlayerAction(client: Socket, payload: PlayerAction) {
|
async handlePlayerAction(client: Socket, payload: PlayerAction) {
|
||||||
// Получаем комнату с данными
|
// Получаем комнату с данными
|
||||||
const room = await this.prisma.room.findUnique({
|
const room = (await this.prisma.room.findUnique({
|
||||||
where: { id: payload.roomId },
|
where: { id: payload.roomId },
|
||||||
include: {
|
include: {
|
||||||
roomPack: true,
|
roomPack: true,
|
||||||
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
||||||
},
|
} as unknown as Prisma.RoomInclude,
|
||||||
});
|
})) as unknown as RoomWithPack | null;
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
client.emit('error', { message: 'Room not found' });
|
client.emit('error', { message: 'Room not found' });
|
||||||
|
|
@ -165,7 +196,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const questions = room.roomPack?.questions as any[] || [];
|
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||||
const question = questions.find(q => q.id === payload.questionId);
|
const question = questions.find(q => q.id === payload.questionId);
|
||||||
|
|
||||||
if (!question) {
|
if (!question) {
|
||||||
|
|
@ -180,8 +211,8 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем revealedAnswers
|
// Обновляем revealedAnswers
|
||||||
const revealed = (room.revealedAnswers as any) || {};
|
const revealed = (room.revealedAnswers as RevealedAnswers) || {};
|
||||||
const currentRevealed: string[] = revealed[payload.questionId] || [];
|
const currentRevealed: string[] = revealed[payload.questionId || ''] || [];
|
||||||
|
|
||||||
if (!currentRevealed.includes(payload.answerId)) {
|
if (!currentRevealed.includes(payload.answerId)) {
|
||||||
currentRevealed.push(payload.answerId);
|
currentRevealed.push(payload.answerId);
|
||||||
|
|
@ -196,12 +227,12 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
// Сохраняем revealedAnswers
|
// Сохраняем revealedAnswers
|
||||||
await this.prisma.room.update({
|
await this.prisma.room.update({
|
||||||
where: { id: payload.roomId },
|
where: { id: payload.roomId },
|
||||||
data: { revealedAnswers: revealed }
|
data: { revealedAnswers: revealed as Prisma.InputJsonValue }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Определяем следующего игрока
|
// Определяем следующего игрока
|
||||||
const participants = room.participants;
|
const participants = room.participants;
|
||||||
const currentIdx = participants.findIndex((p: any) => p.id === payload.participantId);
|
const currentIdx = participants.findIndex((p) => p.id === payload.participantId);
|
||||||
const nextIdx = (currentIdx + 1) % participants.length;
|
const nextIdx = (currentIdx + 1) % participants.length;
|
||||||
const nextPlayerId = participants[nextIdx]?.id;
|
const nextPlayerId = participants[nextIdx]?.id;
|
||||||
|
|
||||||
|
|
@ -240,9 +271,9 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleNextQuestionAction(payload: PlayerAction, room: any) {
|
private async handleNextQuestionAction(payload: PlayerAction, room: RoomWithPack) {
|
||||||
const questions = room.roomPack?.questions as any[] || [];
|
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||||
const currentIdx = questions.findIndex((q: any) => q.id === room.currentQuestionId);
|
const currentIdx = questions.findIndex((q) => q.id === (room.currentQuestionId as string | null));
|
||||||
|
|
||||||
if (currentIdx < questions.length - 1) {
|
if (currentIdx < questions.length - 1) {
|
||||||
const nextQuestion = questions[currentIdx + 1];
|
const nextQuestion = questions[currentIdx + 1];
|
||||||
|
|
@ -250,15 +281,15 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
where: { id: payload.roomId },
|
where: { id: payload.roomId },
|
||||||
data: {
|
data: {
|
||||||
currentQuestionId: nextQuestion.id,
|
currentQuestionId: nextQuestion.id,
|
||||||
currentQuestionIndex: currentIdx + 1 // Для совместимости
|
currentQuestionIndex: currentIdx + 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handlePrevQuestionAction(payload: PlayerAction, room: any) {
|
private async handlePrevQuestionAction(payload: PlayerAction, room: RoomWithPack) {
|
||||||
const questions = room.roomPack?.questions as any[] || [];
|
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||||
const currentIdx = questions.findIndex((q: any) => q.id === room.currentQuestionId);
|
const currentIdx = questions.findIndex((q) => q.id === (room.currentQuestionId as string | null));
|
||||||
|
|
||||||
if (currentIdx > 0) {
|
if (currentIdx > 0) {
|
||||||
const prevQuestion = questions[currentIdx - 1];
|
const prevQuestion = questions[currentIdx - 1];
|
||||||
|
|
@ -266,7 +297,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
where: { id: payload.roomId },
|
where: { id: payload.roomId },
|
||||||
data: {
|
data: {
|
||||||
currentQuestionId: prevQuestion.id,
|
currentQuestionId: prevQuestion.id,
|
||||||
currentQuestionIndex: currentIdx - 1 // Для совместимости
|
currentQuestionIndex: currentIdx - 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +305,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
|
|
||||||
// КЛЮЧЕВОЙ МЕТОД: Отправка полного состояния
|
// КЛЮЧЕВОЙ МЕТОД: Отправка полного состояния
|
||||||
private async broadcastFullState(roomCode: string) {
|
private async broadcastFullState(roomCode: string) {
|
||||||
const room = await this.prisma.room.findUnique({
|
const room = (await this.prisma.room.findUnique({
|
||||||
where: { code: roomCode },
|
where: { code: roomCode },
|
||||||
include: {
|
include: {
|
||||||
participants: {
|
participants: {
|
||||||
|
|
@ -283,21 +314,38 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
},
|
},
|
||||||
roomPack: true,
|
roomPack: true,
|
||||||
host: { select: { id: true, name: true } }
|
host: { select: { id: true, name: true } }
|
||||||
}
|
} as Prisma.RoomInclude,
|
||||||
});
|
})) as unknown as RoomWithPack | null;
|
||||||
|
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
const questions = room.roomPack?.questions as any[] || [];
|
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||||
|
|
||||||
// Инициализация currentQuestionId если не установлен
|
// Инициализация currentQuestionId если не установлен или невалиден
|
||||||
let currentQuestionId = room.currentQuestionId;
|
let currentQuestionId = (room.currentQuestionId as string | null) || null;
|
||||||
|
|
||||||
|
// Проверяем, что currentQuestionId валиден (существует в вопросах)
|
||||||
|
if (currentQuestionId) {
|
||||||
|
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
|
||||||
|
if (!questionExists) {
|
||||||
|
currentQuestionId = null; // Сбрасываем если вопрос удален
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем первый вопрос если нет текущего
|
||||||
if (!currentQuestionId && questions.length > 0) {
|
if (!currentQuestionId && questions.length > 0) {
|
||||||
currentQuestionId = questions[0].id;
|
const firstQuestion = questions[0];
|
||||||
await this.prisma.room.update({
|
// Убеждаемся что id - строка (UUID)
|
||||||
where: { id: room.id },
|
if (firstQuestion.id && typeof firstQuestion.id === 'string') {
|
||||||
data: { currentQuestionId }
|
currentQuestionId = firstQuestion.id;
|
||||||
});
|
await this.prisma.room.update({
|
||||||
|
where: { id: room.id },
|
||||||
|
data: {
|
||||||
|
currentQuestionId: currentQuestionId,
|
||||||
|
currentQuestionIndex: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullState = {
|
const fullState = {
|
||||||
|
|
@ -306,20 +354,20 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
status: room.status,
|
status: room.status,
|
||||||
currentQuestionId: currentQuestionId,
|
currentQuestionId: currentQuestionId,
|
||||||
currentPlayerId: room.currentPlayerId,
|
currentPlayerId: room.currentPlayerId,
|
||||||
revealedAnswers: room.revealedAnswers,
|
revealedAnswers: room.revealedAnswers as RevealedAnswers,
|
||||||
isGameOver: room.isGameOver,
|
isGameOver: room.isGameOver,
|
||||||
hostId: room.hostId,
|
hostId: room.hostId,
|
||||||
participants: room.participants.map(p => ({
|
participants: room.participants.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
userId: p.userId,
|
userId: p.userId,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
role: p.role,
|
role: p.role,
|
||||||
score: p.score
|
score: p.score
|
||||||
})),
|
})),
|
||||||
questions: questions.map((q: any) => ({
|
questions: questions.map((q) => ({
|
||||||
id: q.id,
|
id: q.id,
|
||||||
text: q.text || q.question,
|
text: q.text || q.question || '',
|
||||||
answers: (q.answers || []).map((a: any) => ({
|
answers: (q.answers || []).map((a) => ({
|
||||||
id: a.id,
|
id: a.id,
|
||||||
text: a.text,
|
text: a.text,
|
||||||
points: a.points
|
points: a.points
|
||||||
|
|
@ -355,30 +403,37 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = await this.prisma.room.findUnique({
|
const room = (await this.prisma.room.findUnique({
|
||||||
where: { id: payload.roomId },
|
where: { id: payload.roomId },
|
||||||
include: {
|
include: {
|
||||||
roomPack: true,
|
roomPack: true,
|
||||||
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
||||||
}
|
} as Prisma.RoomInclude,
|
||||||
});
|
})) as unknown as RoomWithPack | null;
|
||||||
|
|
||||||
const questions = room?.roomPack?.questions as any[] || [];
|
if (room) {
|
||||||
const firstQuestion = questions[0];
|
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||||
const firstParticipant = room?.participants[0];
|
const firstQuestion = questions[0];
|
||||||
|
const firstParticipant = room.participants[0];
|
||||||
|
|
||||||
await this.prisma.room.update({
|
// Убеждаемся что firstQuestion.id - строка (UUID)
|
||||||
where: { id: payload.roomId },
|
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
|
||||||
data: {
|
? firstQuestion.id
|
||||||
status: 'WAITING',
|
: null;
|
||||||
currentQuestionIndex: 0,
|
|
||||||
currentQuestionId: firstQuestion?.id || null,
|
await this.prisma.room.update({
|
||||||
revealedAnswers: {},
|
where: { id: payload.roomId },
|
||||||
currentPlayerId: firstParticipant?.id || null,
|
data: {
|
||||||
isGameOver: false,
|
status: 'WAITING',
|
||||||
answeredQuestions: 0,
|
currentQuestionId: firstQuestionId,
|
||||||
},
|
currentQuestionIndex: 0,
|
||||||
});
|
revealedAnswers: {} as Prisma.InputJsonValue,
|
||||||
|
currentPlayerId: firstParticipant?.id || null,
|
||||||
|
isGameOver: false,
|
||||||
|
answeredQuestions: 0,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await this.prisma.participant.updateMany({
|
await this.prisma.participant.updateMany({
|
||||||
where: { roomId: payload.roomId },
|
where: { roomId: payload.roomId },
|
||||||
|
|
@ -396,7 +451,49 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем вопросы через service (который добавит UUID если нужно)
|
||||||
await this.roomsService.updateRoomPack(payload.roomId, payload.questions);
|
await this.roomsService.updateRoomPack(payload.roomId, payload.questions);
|
||||||
|
|
||||||
|
// После обновления вопросов проверяем и обновляем currentQuestionId
|
||||||
|
const room = (await this.prisma.room.findUnique({
|
||||||
|
where: { id: payload.roomId },
|
||||||
|
include: { roomPack: true } as unknown as Prisma.RoomInclude,
|
||||||
|
})) as unknown as RoomWithPack | null;
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||||
|
const currentQuestionId = (room.currentQuestionId as string | null) || null;
|
||||||
|
|
||||||
|
// Проверяем, что currentQuestionId все еще валиден
|
||||||
|
let validQuestionId = currentQuestionId;
|
||||||
|
if (currentQuestionId) {
|
||||||
|
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
|
||||||
|
if (!questionExists) {
|
||||||
|
// Текущий вопрос был удален, устанавливаем первый
|
||||||
|
validQuestionId = questions[0]?.id && typeof questions[0].id === 'string'
|
||||||
|
? questions[0].id
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
} else if (questions.length > 0) {
|
||||||
|
// Если нет currentQuestionId, устанавливаем первый
|
||||||
|
validQuestionId = questions[0].id && typeof questions[0].id === 'string'
|
||||||
|
? questions[0].id
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем currentQuestionId если изменился
|
||||||
|
if (validQuestionId !== currentQuestionId) {
|
||||||
|
const questionIndex = questions.findIndex((q: any) => q.id === validQuestionId);
|
||||||
|
await this.prisma.room.update({
|
||||||
|
where: { id: payload.roomId },
|
||||||
|
data: {
|
||||||
|
currentQuestionId: validQuestionId,
|
||||||
|
currentQuestionIndex: questionIndex >= 0 ? questionIndex : 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.broadcastFullState(payload.roomCode);
|
await this.broadcastFullState(payload.roomCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue