Skip to content

Commit

Permalink
feat(backend/frontend): retake quiz bb-357 (#404)
Browse files Browse the repository at this point in the history
* feat(frontend): onboarding and quiz in order bb-242

* feat(frontend/backend): save user quiz actions bb-242

* feat(frontend): save onboarding user answers bb-242

* feat(frontend): show onboarding on initial bb-242

* feat(frontend): step quiz to introduction  bb-242

* fix(frontend): resolve code style issue bb-242

* feat(frontend/backend): move onboarding to a new route bb-242

* feat(frontend/backend): route validations bb-242

* feat(frontend/backend): onboarding quiz flow bb-242

* fix(frontend): resolve navigation and default value bb-242

* fix(frontend): fix code style inconsistencies bb-242

* fix(frontend): remove unnecessary imports bb-242

* fix(frontend/backend): use boolean instead of answers bb-242

* fix(frontend/backend): has answer validation bb-242

* feat: implemented backend part of feature bb-357

* feat: added swagger for the new endpoint bb-357

* refactor: renamed methos of controller, service, repository an dto for get categories bb-357

* refactor: changed the name of api endpoint bb-357

* fix(frontend/backend): resolve code style errors and rerender bb-242

* fix(frontend/backend): fix merge conflicts bb-242

* fix(frontend): test commit bb-242

* fix(frontend): fix code building bb-242

* refactor: unified endpoints, updated swagger doc, added validation schema bb-357

* refactor: rewritten scoreEntities initialization in findByIds bb-357

* fix: reinstalled node_modules bb-357

* fix: rebuilt packages bb-357

* Revert "fix(frontend): fix code building bb-242"

This reverts commit e6581ba.

* fix(frontend): move logic from root to protected route bb-242

* fix(backend): remove onboarding and quiz answer on entity bb-242

* fix(backend): add onboarding and quiz relation to update bb-242

* fix(frontend): remove dispatch on balance wheel bb-242

* feat: edit mode toggle bb-357

* fix: unexpected switch toggle, :disabled pseudo-class for buttons bb-357

* fix: removed redundant style from edit-mode-switch css module bb-357

* refactor: removed redundant api endpoint bb-357

* refactor: inverted logic of findCategories method of category service bb-357

* refactor: added dot to categories validation message enum bb-357

* refactor: changed isDisabled and isSelected values for editModeSwitch commponent bb-357

* refactor: simplified repository methods, renamed handleModeToggle to onModeToggle bb-357

* fix(frontend/backend): remove unnecessary codes bb-242

* refactor: removed redundant api endpoint from categories bb-357

* fix(frontend): button disabled fix bb-242

* feat: created thunk to get questions by categoryIds bb-357

* refactor: rewritten handle next step as a redux action bb-357

* fix(frontend): resolve code style errors bb-242

* fix(frontend): rename CompletedQuestions bb-242

* fix(frontend): remove unnecessary code bb-242

* fix(frontend): remove repeating code bb-242

* fix(frontend): remove redundant functions bb-242

* refactor: gave better names to slice state variables bb-357

* feat: implemented retake quiz bb-357

* fix: removed extra questions category bug bb-357

* fix: removed redundant check from categoryIdsValidationSchema bb-357

* refactor: remade extractIdsFromAnswerEntities helper into a quiz-answer service method bb-357

* refactor: renamed method findByIds to findByCategoryIds on quiz-question repository bb-357

* fix: double categories quiz on registration bug fixed bb-357

* refactor: moved scoresEditmodal to root page component bb-357

* refactor: made switch component more versatile bb-357

* refactor: rewritten findQuestions method of quiz-question service bb-357

* refactor: made switch component generic bb-357

* refactor: rewritten andWhere id queries into whereIn bb-357

* fix: removed redundant check, added missing prop value to switch on user wheel bb-357

* refactor: changed styling, fixed stuck on analyzing bug bb-357

* fix: onboarding not showing when registering a second user bb-357

* refactor: simplified extract category ids from questions helper bb-357

* refactor: destructured query for findQuestions method of quiz questions service bb-357

* refactor: removed undefined from categoryIds property on QuizAnswersRequestDto bb-357

* refactor: getModal on user-wheel component returns null as default case bb-357

* refactor: changed id names in where clauses on repositories bb-357

* fix: removed redundant check inside of next question reducer function for quiz slice bb-357

* refactor: added first item index constant bb-357

* refactor: rewritten getModal into an arrow function bb-357

* refactor(frontend): update function name, move constant out of component bb-357

---------

Co-authored-by: Mikoyzskie <[email protected]>
Co-authored-by: Farid Shabanov <[email protected]>
Co-authored-by: Farid Shabanov <[email protected]>
  • Loading branch information
4 people authored Sep 18, 2024
1 parent 833d5ed commit bbc02a8
Show file tree
Hide file tree
Showing 64 changed files with 714 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type ControllerRouteParameters = {
preHandlers?: APIPreHandler[];
validation?: {
body?: ValidationSchema;
query?: ValidationSchema;
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class BaseServerApplication implements ServerApplication {
preHandler: preHandlers,
schema: {
body: validation?.body,
query: validation?.query,
},
url: path,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type ServerApplicationRouteParameters = {
preHandlers: APIPreHandler[];
validation?: {
body?: ValidationSchema;
query?: ValidationSchema;
};
};

Expand Down
25 changes: 13 additions & 12 deletions apps/backend/src/modules/categories/category.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ class CategoryRepository implements Repository {
.where({ userId })
.delete();
}
public async deleteUserScoresByCategoryIds(
userId: number,
categoryIds: number[],
): Promise<number> {
return await this.categoryModel
.query()
.from(DatabaseTableName.QUIZ_SCORES)
.whereIn("categoryId", categoryIds)
.andWhere({ userId })
.delete();
}

public async find(id: number): Promise<CategoryEntity | null> {
const category = await this.categoryModel
Expand All @@ -95,22 +106,12 @@ class CategoryRepository implements Repository {
const categories = await this.categoryModel.query().select("*");

return await Promise.all(
categories.map(async (category) => {
const scoresModel = await this.categoryModel
.query()
.from(DatabaseTableName.QUIZ_SCORES)
.where({ categoryId: category.id })
.castTo<CategoryModel[]>();

const scoreEntities = scoresModel.map((score) => {
return CategoryEntity.initializeNew(score);
});

categories.map((category) => {
return CategoryEntity.initialize({
createdAt: category.createdAt,
id: category.id,
name: category.name,
scores: scoreEntities,
scores: [],
updatedAt: category.updatedAt,
});
}),
Expand Down
10 changes: 10 additions & 0 deletions apps/backend/src/modules/categories/category.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ class CategoryService implements Service {
return this.categoryRepository.deleteUserScores(userId);
}

public deleteUserScoresByCategoryIds(
userId: number,
categoryIds: number[],
): Promise<number> {
return this.categoryRepository.deleteUserScoresByCategoryIds(
userId,
categoryIds,
);
}

public async find(id: number): Promise<CategoryDto | null> {
const categoryEntity = await this.categoryRepository.find(id);

Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/modules/quiz-answers/quiz-answer.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ class QuizAnswerRepository implements Repository {
.delete();
}

public async deleteUserAnswersByAnswerIds(
userId: number,
answerIds: number[],
): Promise<number> {
return await this.quizAnswerModel
.query()
.from(DatabaseTableName.QUIZ_ANSWERS_TO_USERS)
.whereIn("answerId", answerIds)
.andWhere({ userId })
.delete();
}

public async find(id: number): Promise<null | QuizAnswerEntity> {
const answer = await this.quizAnswerModel
.query()
Expand Down
97 changes: 93 additions & 4 deletions apps/backend/src/modules/quiz-answers/quiz-answer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class QuizAnswerService implements Service {
this.quizQuestionService = quizQuestionService;
}

public convertAnswerEntityToDto(
private convertAnswerEntityToDto(
answerEntity: QuizAnswerEntity,
): QuizAnswerDto {
const answer = answerEntity.toObject();
Expand All @@ -54,6 +54,20 @@ class QuizAnswerService implements Service {
};
}

private extractIdsFromAnswerEntities(
answerEntities: QuizAnswerEntity[],
): number[] {
const answerIds: number[] = [];

for (const answerEntity of answerEntities) {
const answer = answerEntity.toObject();

answerIds.push(answer.id);
}

return answerIds;
}

public async create(payload: QuizAnswerRequestDto): Promise<QuizAnswerDto> {
const answerEntity = await this.quizAnswerRepository.create(
QuizAnswerEntity.initializeNew(payload),
Expand All @@ -62,6 +76,52 @@ class QuizAnswerService implements Service {
return this.convertAnswerEntityToDto(answerEntity);
}

public async createAllUserAnswers({
answerIds,
userId,
}: UserAnswersRequestDto): Promise<QuizAnswersResponseDto> {
const existingAnswers =
await this.quizAnswerRepository.findByIds(answerIds);

if (existingAnswers.length !== answerIds.length) {
throw new QuizError({
message: ErrorMessage.REQUESTED_ENTITY_NOT_FOUND,
status: HTTPCode.NOT_FOUND,
});
}

const questionsCount = await this.quizQuestionService.countAll();

if (questionsCount !== existingAnswers.length) {
throw new QuizError({
message: ErrorMessage.INSUFFICIENT_ANSWERS,
status: HTTPCode.BAD_REQUEST,
});
}

const answerEntities = existingAnswers.map((answer) => answer.toObject());
const questionIds = answerEntities.map((answer) => answer.questionId);
const uniqueQuestionIds = new Set(questionIds);

if (uniqueQuestionIds.size !== questionIds.length) {
throw new QuizError({
message: ErrorMessage.DUPLICATE_QUESTION_ANSWER,
status: HTTPCode.BAD_REQUEST,
});
}

await this.quizAnswerRepository.deleteUserAnswers(userId);
const userAnswers = await this.quizAnswerRepository.createUserAnswers(
userId,
answerIds,
);

await this.categoryService.deleteUserScores(userId);
const { items } = await this.createScores({ answerIds, userId });

return { scores: items, userAnswers };
}

public async createScores({
answerIds,
userId,
Expand Down Expand Up @@ -104,6 +164,23 @@ class QuizAnswerService implements Service {

public async createUserAnswers({
answerIds,
categoryIds,
userId,
}: UserAnswersRequestDto): Promise<QuizAnswersResponseDto> {
if (!categoryIds) {
return await this.createAllUserAnswers({ answerIds, userId });
}

return await this.createUserAnswersByCategories({
answerIds,
categoryIds,
userId,
});
}

public async createUserAnswersByCategories({
answerIds,
categoryIds,
userId,
}: UserAnswersRequestDto): Promise<QuizAnswersResponseDto> {
const existingAnswers =
Expand All @@ -116,7 +193,9 @@ class QuizAnswerService implements Service {
});
}

const questionsCount = await this.quizQuestionService.countAll();
const questionsCount = await this.quizQuestionService.countByCategoryIds(
categoryIds as number[],
);

if (questionsCount !== existingAnswers.length) {
throw new QuizError({
Expand All @@ -136,13 +215,23 @@ class QuizAnswerService implements Service {
});
}

await this.quizAnswerRepository.deleteUserAnswers(userId);
const existingAnswersIds =
this.extractIdsFromAnswerEntities(existingAnswers);

await this.quizAnswerRepository.deleteUserAnswersByAnswerIds(
userId,
existingAnswersIds,
);

const userAnswers = await this.quizAnswerRepository.createUserAnswers(
userId,
answerIds,
);

await this.categoryService.deleteUserScores(userId);
await this.categoryService.deleteUserScoresByCategoryIds(
userId,
categoryIds as number[],
);
const { items } = await this.createScores({ answerIds, userId });

return { scores: items, userAnswers };
Expand Down
6 changes: 5 additions & 1 deletion apps/backend/src/modules/quiz-questions/libs/types/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export { type QuizQuestionDto, type QuizQuestionRequestDto } from "shared";
export {
type CategoriesGetRequestQueryDto,
type QuizQuestionDto,
type QuizQuestionRequestDto,
} from "shared";
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ class QuizQuestionRepository implements Repository {

return Number(questionModelCount[FIRST_ELEMENT_INDEX].count);
}
public async countByCategoryIds(categoryIds: number[]): Promise<number> {
const questionModelCount = await this.quizQuestionModel
.query()
.whereIn("categoryId", categoryIds)
.count()
.castTo<[{ count: string }]>();

return Number(questionModelCount[FIRST_ELEMENT_INDEX].count);
}

public async create(entity: QuizQuestionEntity): Promise<QuizQuestionEntity> {
const { categoryId, label } = entity.toNewObject();
Expand Down Expand Up @@ -103,6 +112,38 @@ class QuizQuestionRepository implements Repository {
);
}

public async findByCategoryIds(
categoryIds: number[],
): Promise<QuizQuestionEntity[]> {
const questions = await this.quizQuestionModel
.query()
.whereIn("categoryId", categoryIds)
.select("*");

return await Promise.all(
questions.map(async (question) => {
const answersModel = await this.quizQuestionModel
.relatedQuery(RelationName.QUIZ_ANSWERS)
.for(question.id)
.select("*")
.castTo<QuizAnswerModel[]>();

const answerEntities = answersModel.map((answer) => {
return QuizAnswerEntity.initialize(answer);
});

return QuizQuestionEntity.initialize({
answers: answerEntities,
categoryId: question.categoryId,
createdAt: question.createdAt,
id: question.id,
label: question.label,
updatedAt: question.updatedAt,
});
}),
);
}

public async update(
id: number,
payload: Partial<QuizQuestionRequestDto>,
Expand Down
46 changes: 46 additions & 0 deletions apps/backend/src/modules/quiz-questions/quiz-question.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type QuizAnswerEntity,
} from "../quiz-answers/quiz-answers.js";
import {
type CategoriesGetRequestQueryDto,
type QuizQuestionDto,
type QuizQuestionRequestDto,
} from "./libs/types/types.js";
Expand Down Expand Up @@ -43,6 +44,10 @@ class QuizQuestionService implements Service {
return await this.quizQuestionRepository.countAll();
}

public async countByCategoryIds(categoryIds: number[]): Promise<number> {
return await this.quizQuestionRepository.countByCategoryIds(categoryIds);
}

public async create(
payload: QuizQuestionRequestDto,
): Promise<QuizQuestionDto> {
Expand Down Expand Up @@ -89,6 +94,47 @@ class QuizQuestionService implements Service {
return { items: result };
}

public async findQuestions(
query: CategoriesGetRequestQueryDto,
): Promise<{ items: QuizQuestionDto[][] }> {
const { categoryIds } = query;

if (!categoryIds) {
return await this.findAll();
}

return await this.findQuestionsByCategoryIds(
JSON.parse(query.categoryIds) as number[],
);
}

public async findQuestionsByCategoryIds(
categoryIds: number[],
): Promise<{ items: QuizQuestionDto[][] }> {
const questions =
await this.quizQuestionRepository.findByCategoryIds(categoryIds);

const items = questions.map((questionEntity) => {
return this.convertQuestionEntityToDto(questionEntity);
});

const groupedByCategory: Record<number, QuizQuestionDto[]> = {};

for (const question of items) {
const { categoryId } = question;

if (!groupedByCategory[categoryId]) {
groupedByCategory[categoryId] = [];
}

groupedByCategory[categoryId].push(question);
}

const result = Object.values(groupedByCategory);

return { items: result };
}

public async update(
id: number,
payload: Partial<QuizQuestionRequestDto>,
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/modules/quiz/libs/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { type CategoriesGetRequestQueryDto } from "shared";
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { quizUserAnswersValidationSchema } from "shared";
export {
categoryIdsValidationSchema,
quizUserAnswersValidationSchema,
} from "shared";
Loading

0 comments on commit bbc02a8

Please sign in to comment.