diff --git a/schema.gql b/schema.gql index 6780fcc50c98d4013aba16b49f112a60418f401e..95c03f6ff58a429c01ad3e3644217adfa56fdd6c 100644 --- a/schema.gql +++ b/schema.gql @@ -27,27 +27,58 @@ type Quiz { questions: [Question!]! } +"""deleting status""" +type StatusModel { + success: Boolean! +} + type Query { quizzes: [Quiz!]! + getQuizById(id: String!): Quiz + getQuestionById(id: String!): Question } type Mutation { - addQuiz(newQuizData: NewQuizInput!): Quiz! + deleteQuizById(id: String!): StatusModel! + addQuiz(newQuizData: CreateQuizInput!): Quiz! + updateQuizById(updateQuizData: UpdateQuizInput!): Quiz! + deleteQuestionById(id: String!): StatusModel! + updateQuestionById(updateQuestionData: UpdateQuestionInput!): Question! + deleteAnswerById(id: String!): StatusModel! + updateAnswerById(updateAnsData: UpdateAnswerInput!): Answer! + updateRightAnswer(id: String!): [Answer!]! } -input NewQuizInput { +input CreateQuizInput { name: String! - questions: [NewQuestionInput!]! + questions: [CreateQuestionInput!]! } -input NewQuestionInput { +input CreateQuestionInput { content: String! imgURL: String = null - answers: [NewAnswerInput!]! + answers: [CreateAnswerInput!]! } -input NewAnswerInput { +input CreateAnswerInput { text: String! imgURL: String isCorrect: Boolean = false } + +input UpdateQuizInput { + id: String! + name: String! +} + +input UpdateQuestionInput { + id: String! + content: String! + imgURL: String = null +} + +input UpdateAnswerInput { + id: String! + text: String! + imgURL: String +} diff --git a/src/answers/answers.resolver.ts b/src/answers/answers.resolver.ts index f0dba46cb78cdb0250616136f4c2219d43ff6c18..f139965e9b2a359d14613f83ec7b39aae28d76b5 100644 --- a/src/answers/answers.resolver.ts +++ b/src/answers/answers.resolver.ts @@ -1,7 +1,28 @@ -import { Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { AnswersService } from './answers.service'; +import { StatusModel } from '../common/models/status.model'; +import { Answer } from './models/answer.model'; +import { UpdateAnswerInput } from './dto/update-answer.input'; @Resolver() export class AnswersResolver { constructor(private readonly answersService: AnswersService) {} + + @Mutation((returns) => StatusModel) + async deleteAnswerById(@Args('id') id: string): Promise { + const success = await this.answersService.deleteAnswerById(id); + return { success }; + } + + @Mutation((returns) => Answer) + async updateAnswerById( + @Args('updateAnsData') updateAnsData: UpdateAnswerInput, + ): Promise { + return await this.answersService.updateAnswerById(updateAnsData); + } + + @Mutation((returns) => [Answer]) + async updateRightAnswer(@Args('id') id: string): Promise { + return await this.answersService.updateRightAnswer(id); + } } diff --git a/src/answers/answers.service.ts b/src/answers/answers.service.ts index f5d3ab252840618a403050e738b712a166825edf..623dbded87c29aa82cef2e693a89a1b7d89941f4 100644 --- a/src/answers/answers.service.ts +++ b/src/answers/answers.service.ts @@ -2,7 +2,13 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Answer } from './models/answer.model'; -import { NewAnswerInput } from './dto/new-answer.input'; +import { CreateAnswerInput } from './dto/create-answer.input'; +import { UpdateAnswerInput } from './dto/update-answer.input'; +import { + CREATE_ANSWER_BEFORE_DELETE, + DELETE_CORRECT_ANSWER_ERROR, + NO_ANSWER, +} from '../constants'; @Injectable() export class AnswersService { @@ -11,7 +17,45 @@ export class AnswersService { private readonly answersRepository: Repository, ) {} - async createAnswer(data: NewAnswerInput[]) { + async createAnswer(data: CreateAnswerInput[]) { return await this.answersRepository.save(data); } + + async deleteAnswerById(id: string): Promise { + const answer = await this.answersRepository.findOne(id, { + relations: ['question', 'question.answers'], + }); + + if (!answer) throw new Error(NO_ANSWER); + else if (answer.isCorrect) throw new Error(DELETE_CORRECT_ANSWER_ERROR); + else if (answer.question.answers.length < 3) { + throw new Error(CREATE_ANSWER_BEFORE_DELETE); + } + + const { affected } = await this.answersRepository.delete(id); + return !!affected; + } + + async updateAnswerById(data: UpdateAnswerInput): Promise { + const answer = await this.answersRepository.findOne(data.id); + if (!answer) throw new Error(NO_ANSWER); + + return await this.answersRepository.save({ + ...answer, + ...data, + }); + } + + async updateRightAnswer(id: string): Promise { + const answer = await this.answersRepository.findOne(id, { + relations: ['question', 'question.answers'], + }); + const changedAnswer = answer.question.answers.find( + (elem) => elem.isCorrect === true, + ); + changedAnswer.isCorrect = false; + answer.isCorrect = true; + + return await this.answersRepository.save([answer, changedAnswer]); + } } diff --git a/src/answers/dto/new-answer.input.ts b/src/answers/dto/create-answer.input.ts similarity index 90% rename from src/answers/dto/new-answer.input.ts rename to src/answers/dto/create-answer.input.ts index 290878559a98cd2a4cebdea2a61aa143e913682c..9451e8966453b9849848a7f9d2710d10fca1f7a8 100644 --- a/src/answers/dto/new-answer.input.ts +++ b/src/answers/dto/create-answer.input.ts @@ -2,7 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; import { IsOptional, Length } from 'class-validator'; @InputType() -export class NewAnswerInput { +export class CreateAnswerInput { @Field({ nullable: false }) @Length(1, 30) text: string; diff --git a/src/answers/dto/update-answer.input.ts b/src/answers/dto/update-answer.input.ts new file mode 100644 index 0000000000000000000000000000000000000000..6daa9fa62dc028088ef345e1d44ccb99ea0aebe1 --- /dev/null +++ b/src/answers/dto/update-answer.input.ts @@ -0,0 +1,17 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsOptional, IsUUID, Length } from 'class-validator'; + +@InputType() +export class UpdateAnswerInput { + @Field({ nullable: false }) + @IsUUID('all') + id: string; + + @Field({ nullable: false }) + @Length(1, 30) + text: string; + + @Field({ nullable: true }) + @IsOptional() + imgURL?: string; +} diff --git a/src/answers/models/answer.model.ts b/src/answers/models/answer.model.ts index 340fadf0b0cf363d27f4d918229322763b29dd9c..febdbca48f50586a4dbb9f4271c3438204c96917 100644 --- a/src/answers/models/answer.model.ts +++ b/src/answers/models/answer.model.ts @@ -31,6 +31,8 @@ export class Answer { isCorrect: boolean; @Field((type) => Question, { nullable: false }) - @ManyToOne(() => Question, (question) => question.answers) + @ManyToOne(() => Question, (question) => question.answers, { + onDelete: 'CASCADE', + }) question: Question; } diff --git a/src/app.module.ts b/src/app.module.ts index 35f80d94ade351f7a3a75ad3335daaf88747c2d5..75843f1bf0c283e6d007c66d739ff2ab83856356 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -19,7 +19,7 @@ import { Answer } from './answers/models/answer.model'; }), TypeOrmModule.forRoot({ type: 'postgres', - host: process.env.POSTGRES_HOST || 'postgres', + host: process.env.POSTGRES_HOST || '127.0.0.1', port: Number(process.env.POSTGRES_PORT) || 5433, username: process.env.POSTGRES_USER || 'postgres', password: process.env.POSTGRES_PASSWORD || 'root', diff --git a/src/common/models/status.model.ts b/src/common/models/status.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..6bcdd3bea5dea57c1acf4574a5e6949d89506dfc --- /dev/null +++ b/src/common/models/status.model.ts @@ -0,0 +1,7 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType({ description: 'deleting status' }) +export class StatusModel { + @Field({ nullable: false }) + success: boolean; +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f417d01d4310f4baece94655b33dd22de931463 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,14 @@ +export const NO_ANSWER = 'no such answer'; +export const DELETE_CORRECT_ANSWER_ERROR = 'you cant delete right answer'; +export const CREATE_ANSWER_BEFORE_DELETE = 'create new answer before delete'; +export const NO_QUIZ = 'no such quiz'; +export const NO_GAME = 'no such game'; +export const GAME_HAS_STARTED = 'the game has already started'; +export const GAME_NOT_ACTIVATED = 'the game is not activated'; +export const NO_PLAYER = 'no such player in the game'; +export const GAME_IS_NOT_PLAYING = 'the game is not being played now'; +export const ALREADY_ANSWERED = 'player has already answered the question'; +export const SMTHNG_WENT_WRONG = 'something went wrong'; +export const GAME_NOT_FINISHED = 'the game is not finished yet'; +export const CREAT_QUESTION_BEFORE_DELETE = 'create new question before delete'; +export const NO_QUESTION = 'no such question'; diff --git a/src/questions/dto/new-question.input.ts b/src/questions/dto/create-question.input.ts similarity index 66% rename from src/questions/dto/new-question.input.ts rename to src/questions/dto/create-question.input.ts index 505817eb0cbe8b80716c647966348012f0ff51c5..e8e0afca587acfdb5f63260d9a7a0d2d20c08dc5 100644 --- a/src/questions/dto/new-question.input.ts +++ b/src/questions/dto/create-question.input.ts @@ -7,10 +7,10 @@ import { Length, MinLength, } from 'class-validator'; -import { NewAnswerInput } from '../../answers/dto/new-answer.input'; +import { CreateAnswerInput } from '../../answers/dto/create-answer.input'; @InputType() -export class NewQuestionInput { +export class CreateQuestionInput { @Field({ nullable: false }) @Length(2, 30) content: string; @@ -20,9 +20,9 @@ export class NewQuestionInput { @MinLength(5) imgURL?: string; - @Field((type) => [NewAnswerInput], { nullable: false }) + @Field((type) => [CreateAnswerInput], { nullable: false }) @IsArray() @ArrayMinSize(2) @ArrayMaxSize(8) - answers: NewAnswerInput[]; + answers: CreateAnswerInput[]; } diff --git a/src/questions/dto/update-question.input.ts b/src/questions/dto/update-question.input.ts new file mode 100644 index 0000000000000000000000000000000000000000..b07c27d12004f74c8fa6406738afd113d9c84ef5 --- /dev/null +++ b/src/questions/dto/update-question.input.ts @@ -0,0 +1,18 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsOptional, IsUUID, Length, MinLength } from 'class-validator'; + +@InputType() +export class UpdateQuestionInput { + @Field({ nullable: false }) + @IsUUID('all') + id: string; + + @Field({ nullable: false }) + @Length(2, 30) + content: string; + + @Field({ nullable: true, defaultValue: null }) + @IsOptional() + @MinLength(5) + imgURL?: string; +} diff --git a/src/questions/models/question.model.ts b/src/questions/models/question.model.ts index f1a840ac7cb1a1656aef57448723145345e5ace1..5637efebe0902de170524d581c044997c3f84a26 100644 --- a/src/questions/models/question.model.ts +++ b/src/questions/models/question.model.ts @@ -27,10 +27,14 @@ export class Question { imgUrl?: string | null; @Field((type) => Quiz) - @ManyToOne(() => Quiz, (quiz) => quiz.questions) + @ManyToOne(() => Quiz, (quiz) => quiz.questions, { + onDelete: 'CASCADE', + }) quiz: Quiz; @Field((type) => [Answer]) - @OneToMany(() => Answer, (answer) => answer.question) + @OneToMany(() => Answer, (answer) => answer.question, { + onDelete: 'CASCADE', + }) answers: Answer[]; } diff --git a/src/questions/questions.module.ts b/src/questions/questions.module.ts index 4b6296b8b67e85bcfa215dbcc379e164b43b9a44..fb2cf38cfca9540a4b7cc28d183600cf25b341ff 100644 --- a/src/questions/questions.module.ts +++ b/src/questions/questions.module.ts @@ -4,12 +4,14 @@ import { QuestionsResolver } from './questions.resolver'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Question } from './models/question.model'; import { AnswersModule } from '../answers/answers.module'; +import { QuizzesModule } from '../quizzes/quizzes.module'; @Module({ providers: [QuestionsService, QuestionsResolver], imports: [ TypeOrmModule.forFeature([Question]), forwardRef(() => AnswersModule), + forwardRef(() => QuizzesModule), ], exports: [QuestionsService], }) diff --git a/src/questions/questions.resolver.ts b/src/questions/questions.resolver.ts index fbc65ca0e47f0f6f903e78f8b8de0e480f28a7a1..241bea42171a655ae1bb8c39fe015262174b0628 100644 --- a/src/questions/questions.resolver.ts +++ b/src/questions/questions.resolver.ts @@ -1,22 +1,28 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Question } from './models/question.model'; import { QuestionsService } from './questions.service'; -import { NewQuestionInput } from './dto/new-question.input'; +import { StatusModel } from '../common/models/status.model'; +import { UpdateQuestionInput } from './dto/update-question.input'; @Resolver((of: Question) => Question) export class QuestionsResolver { constructor(private readonly questionsService: QuestionsService) {} - // @Query((returns) => Question) - // async getQuestionById(@Args('id') id: string): Promise { - // const res: Question = await this.questionsService.getById(id); - // return res; - // } + @Query((returns) => Question, { nullable: true }) + async getQuestionById(@Args('id') id: string): Promise { + return await this.questionsService.getById(id); + } - // @Mutation((returns) => Question) - // async addQuestion( - // @Args('newQuestionData') newQuestionData: NewQuestionInput, - // ): Promise { - // return await this.questionsService.createNewQuestion(newQuestionData); - // } + @Mutation((returns) => StatusModel) + async deleteQuestionById(@Args('id') id: string): Promise { + const status = await this.questionsService.deleteQuestionById(id); + return { success: status }; + } + + @Mutation((returns) => Question) + async updateQuestionById( + @Args('updateQuestionData') updateQuestionData: UpdateQuestionInput, + ): Promise { + return await this.questionsService.updateQuestionById(updateQuestionData); + } } diff --git a/src/questions/questions.service.ts b/src/questions/questions.service.ts index fbd7eab1fceda1993056e9e8faf08ef78277ee17..dc5d0fffed0633de868a4b141489aaec61d26753 100644 --- a/src/questions/questions.service.ts +++ b/src/questions/questions.service.ts @@ -1,9 +1,12 @@ -import { Injectable } from '@nestjs/common'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Question } from './models/question.model'; import { Repository } from 'typeorm'; -import { NewQuestionInput } from './dto/new-question.input'; +import { CreateQuestionInput } from './dto/create-question.input'; import { AnswersService } from '../answers/answers.service'; +import { UpdateQuestionInput } from './dto/update-question.input'; +import { QuizzesService } from '../quizzes/quizzes.service'; +import { CREAT_QUESTION_BEFORE_DELETE, NO_QUESTION } from '../constants'; @Injectable() export class QuestionsService { @@ -11,18 +14,44 @@ export class QuestionsService { @InjectRepository(Question) private readonly questionRepository: Repository, private readonly answerService: AnswersService, + @Inject(forwardRef(() => QuizzesService)) + private readonly quizService: QuizzesService, ) {} - // async getById(id: string): Promise { - // return await this.questionRepository.findOne({ - // where: id, - // }); - // } + async getById(id: string): Promise { + return await this.questionRepository.findOne(id, { + relations: ['answers'], + }); + } - async createNewQuestion(data: NewQuestionInput[]) { + async createNewQuestion(data: CreateQuestionInput[]) { for (const elem of data) { await this.answerService.createAnswer(elem.answers); } return await this.questionRepository.save(data); } + + async deleteQuestionById(id: string): Promise { + const question = await this.questionRepository.findOne(id, { + relations: ['quiz', 'quiz.questions'], + }); + + if (!question) throw new Error(NO_QUESTION); + else if (question.quiz.questions.length < 2) { + throw new Error(CREAT_QUESTION_BEFORE_DELETE); + } + + const { affected } = await this.questionRepository.delete(id); + return !!affected; + } + + async updateQuestionById(data: UpdateQuestionInput): Promise { + const question = await this.questionRepository.findOne(data.id); + if (!question) throw new Error(NO_QUESTION); + + return await this.questionRepository.save({ + ...question, + ...data, + }); + } } diff --git a/src/quizzes/dto/create-quiz.input.ts b/src/quizzes/dto/create-quiz.input.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4819e448e853e9bee70dd6640a6a29573b0051a --- /dev/null +++ b/src/quizzes/dto/create-quiz.input.ts @@ -0,0 +1,16 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { ArrayMaxSize, ArrayMinSize, IsArray, Length } from 'class-validator'; +import { CreateQuestionInput } from '../../questions/dto/create-question.input'; + +@InputType() +export class CreateQuizInput { + @Field({ nullable: false }) + @Length(2, 30) + name: string; + + @Field((type) => [CreateQuestionInput], { nullable: false }) + @IsArray() + @ArrayMinSize(1) + @ArrayMaxSize(30) + questions: CreateQuestionInput[]; +} diff --git a/src/quizzes/dto/new-quiz.input.ts b/src/quizzes/dto/new-quiz.input.ts deleted file mode 100644 index 6e5bac3ae8dae2d117c8304a08c313fec13bd79b..0000000000000000000000000000000000000000 --- a/src/quizzes/dto/new-quiz.input.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; -import { ArrayMinSize, IsArray, Length } from 'class-validator'; -import { NewQuestionInput } from '../../questions/dto/new-question.input'; - -@InputType() -export class NewQuizInput { - @Field({ nullable: false }) - @Length(2, 30) - name: string; - - @Field((type) => [NewQuestionInput], { nullable: false }) - @IsArray() - @ArrayMinSize(1) - questions: NewQuestionInput[]; -} diff --git a/src/quizzes/dto/update-quiz.input.ts b/src/quizzes/dto/update-quiz.input.ts new file mode 100644 index 0000000000000000000000000000000000000000..73c4160e4ec08dfe3ff59f597923595f9b8303d2 --- /dev/null +++ b/src/quizzes/dto/update-quiz.input.ts @@ -0,0 +1,13 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsUUID, Length } from 'class-validator'; + +@InputType() +export class UpdateQuizInput { + @Field({ nullable: false }) + @IsUUID('all') + id: string; + + @Field({ nullable: false }) + @Length(2, 30) + name: string; +} diff --git a/src/quizzes/models/quiz.model.ts b/src/quizzes/models/quiz.model.ts index 5d0645ff29f02cc753e8a781ebfa283da67505c4..22cbf1ef02bf87a4db0521b528c52f720609d826 100644 --- a/src/quizzes/models/quiz.model.ts +++ b/src/quizzes/models/quiz.model.ts @@ -21,6 +21,8 @@ export class Quiz { name: string; @Field((type) => [Question]) - @OneToMany(() => Question, (question) => question.quiz) + @OneToMany(() => Question, (question) => question.quiz, { + onDelete: 'CASCADE', + }) questions: Question[]; } diff --git a/src/quizzes/quizzes.module.ts b/src/quizzes/quizzes.module.ts index eb71a4edd0d473592c4c00a03bc9354306bcd4d6..eddb1bf042c88d621e824fdafd5936322a4a45c6 100644 --- a/src/quizzes/quizzes.module.ts +++ b/src/quizzes/quizzes.module.ts @@ -11,5 +11,6 @@ import { QuestionsModule } from '../questions/questions.module'; TypeOrmModule.forFeature([Quiz]), forwardRef(() => QuestionsModule), ], + exports: [QuizzesService], }) export class QuizzesModule {} diff --git a/src/quizzes/quizzes.resolver.ts b/src/quizzes/quizzes.resolver.ts index b68229c8d2d5c81631d1f5d3825ad1a924461b35..197ea499eed6374657ce5c592d1db1a86e1a5fe2 100644 --- a/src/quizzes/quizzes.resolver.ts +++ b/src/quizzes/quizzes.resolver.ts @@ -1,19 +1,39 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Quiz } from './models/quiz.model'; import { QuizzesService } from './quizzes.service'; -import { NewQuizInput } from './dto/new-quiz.input'; +import { CreateQuizInput } from './dto/create-quiz.input'; +import { StatusModel } from '../common/models/status.model'; +import { UpdateQuizInput } from './dto/update-quiz.input'; @Resolver((of: Quiz) => Quiz) export class QuizzesResolver { constructor(private readonly quizzesService: QuizzesService) {} - @Query((returns) => [Quiz]) + @Query((returns) => [Quiz], { nullable: false }) async quizzes(): Promise { return await this.quizzesService.findAll(); } + @Query((returns) => Quiz, { nullable: true }) + async getQuizById(@Args('id') id: string) { + return await this.quizzesService.getQuizById(id); + } + + @Mutation((returns) => StatusModel) + async deleteQuizById(@Args('id') id: string) { + const res = await this.quizzesService.deleteQuizById(id); + return { success: res }; + } + @Mutation((returns) => Quiz) - async addQuiz(@Args('newQuizData') newQuizData: NewQuizInput) { + async addQuiz(@Args('newQuizData') newQuizData: CreateQuizInput) { return await this.quizzesService.addQuiz(newQuizData); } + + @Mutation((returns) => Quiz) + async updateQuizById( + @Args('updateQuizData') updateQuizData: UpdateQuizInput, + ) { + return this.quizzesService.updateQuizById(updateQuizData); + } } diff --git a/src/quizzes/quizzes.service.ts b/src/quizzes/quizzes.service.ts index 27a239c2b7822bf3556851e4df080357f3428233..73bfcc801247d4b9b02292aa612d3f505aaf6103 100644 --- a/src/quizzes/quizzes.service.ts +++ b/src/quizzes/quizzes.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; import { Quiz } from './models/quiz.model'; -import { NewQuizInput } from './dto/new-quiz.input'; +import { CreateQuizInput } from './dto/create-quiz.input'; import { InjectRepository } from '@nestjs/typeorm'; import { QuestionsService } from '../questions/questions.service'; +import { UpdateQuizInput } from './dto/update-quiz.input'; @Injectable() export class QuizzesService { @@ -19,8 +20,25 @@ export class QuizzesService { }); } - async addQuiz(data: NewQuizInput) { + async addQuiz(data: CreateQuizInput): Promise { await this.questionsService.createNewQuestion(data.questions); return await this.quizRepository.save(data); } + + async getQuizById(id: string): Promise { + return await this.quizRepository.findOne(id, { + relations: ['questions', 'questions.answers'], + }); + } + + async deleteQuizById(id: string): Promise { + const { affected } = await this.quizRepository.delete(id); + return !!affected; + } + + async updateQuizById(data: UpdateQuizInput): Promise { + return await this.quizRepository.save({ + ...data, + }); + } }