From e5c66dc9a8fd72b77a56817671128c06a21ee8e6 Mon Sep 17 00:00:00 2001 From: lynn Date: Sat, 31 Jan 2026 06:51:31 +0100 Subject: [PATCH 1/6] git puzzle --- .../providers/getAll-puzzle.provider.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/src/puzzles/providers/getAll-puzzle.provider.ts b/backend/src/puzzles/providers/getAll-puzzle.provider.ts index 050fb49..49f3a39 100644 --- a/backend/src/puzzles/providers/getAll-puzzle.provider.ts +++ b/backend/src/puzzles/providers/getAll-puzzle.provider.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Puzzle } from '../entities/puzzle.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm/repository/Repository'; -import { paginate } from '../../common/pagination/paginate'; +import { FindOptionsWhere } from 'typeorm'; import { PuzzleQueryDto } from '../dtos/puzzle-query.dto'; @Injectable() @@ -15,19 +15,31 @@ export class GetAllPuzzlesProvider { public async findAll(query: PuzzleQueryDto) { const { categoryId, difficulty, page = 1, limit = 10 } = query; - const qb = this.puzzleRepo - .createQueryBuilder('puzzle') - .leftJoinAndSelect('puzzle.category', 'category') - .orderBy('puzzle.createdAt', 'DESC'); + const where: FindOptionsWhere = {}; if (categoryId) { - qb.andWhere('puzzle.categoryId = :categoryId', { categoryId }); + where.categoryId = categoryId; } if (difficulty) { - qb.andWhere('puzzle.difficulty = :difficulty', { difficulty }); + where.difficulty = difficulty; } - return paginate(qb, page, limit); + const [data, total] = await this.puzzleRepo.findAndCount({ + where, + relations: ['category'], + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + data, + meta: { + page, + limit, + total, + }, + }; } } From e9b802871ea723f3febe1e8231105e9881586a8d Mon Sep 17 00:00:00 2001 From: lynn Date: Sat, 31 Jan 2026 06:51:51 +0100 Subject: [PATCH 2/6] puzzle service --- .../src/puzzles/providers/puzzles.service.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/backend/src/puzzles/providers/puzzles.service.ts b/backend/src/puzzles/providers/puzzles.service.ts index 1e2c9ba..97ec99b 100644 --- a/backend/src/puzzles/providers/puzzles.service.ts +++ b/backend/src/puzzles/providers/puzzles.service.ts @@ -42,15 +42,22 @@ export class PuzzlesService { // GET /puzzles/daily-quest // ------------------------------- async getDailyQuestPuzzles(): Promise { - const puzzles = await this.puzzleRepo - .createQueryBuilder('puzzle') - .innerJoinAndSelect('puzzle.category', 'category') - .where('category.isActive = true') - .orderBy('RANDOM()') // PostgreSQL-safe randomness - .limit(5) - .getMany(); - - return puzzles; + const puzzles = await this.puzzleRepo.find({ + where: { + category: { + isActive: true, + }, + }, + relations: ['category'], + }); + + // Fisher-Yates shuffle for randomized order similar to ORDER BY RANDOM() + for (let i = puzzles.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [puzzles[i], puzzles[j]] = [puzzles[j], puzzles[i]]; + } + + return puzzles.slice(0, 5); } public async findAll(query: PuzzleQueryDto) { From b6046f2b2b8a14fd64a833138becf7eb0b96083e Mon Sep 17 00:00:00 2001 From: lynn Date: Sat, 31 Jan 2026 07:16:23 +0100 Subject: [PATCH 3/6] puzzle service --- .../providers/get-category-stats.provider.ts | 44 ++++++++----------- .../providers/get-overall-stats.provider.ts | 42 ++++++++++-------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/backend/src/progress/providers/get-category-stats.provider.ts b/backend/src/progress/providers/get-category-stats.provider.ts index d93c3f5..ac011ac 100644 --- a/backend/src/progress/providers/get-category-stats.provider.ts +++ b/backend/src/progress/providers/get-category-stats.provider.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { FindOptionsWhere, Repository } from 'typeorm'; import { UserProgress } from '../entities/progress.entity'; interface CategoryStatsRaw { @@ -21,20 +21,17 @@ export class GetCategoryStatsProvider { ) {} async getCategoryStats(userId: string, categoryId: string) { - const result = await this.progressRepo - .createQueryBuilder('progress') - .select('progress.categoryId', 'categoryId') - .addSelect('COUNT(*)', 'totalAttempts') - .addSelect( - 'SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', - 'correctAnswers', - ) - .where('progress.userId = :userId', { userId }) - .andWhere('progress.categoryId = :categoryId', { categoryId }) - .groupBy('progress.categoryId') - .getRawOne(); + const where: FindOptionsWhere = { + userId, + categoryId, + }; - if (!result) { + const progressRecords = await this.progressRepo.find({ + where, + relations: ['category'], + }); + + if (progressRecords.length === 0) { return { categoryId, categoryName: '', @@ -44,25 +41,22 @@ export class GetCategoryStatsProvider { }; } - const totalAttempts = parseInt(result.totalAttempts, 10) || 0; - const correctAnswers = parseInt(result.correctAnswers, 10) || 0; + const totalAttempts = progressRecords.length; + const correctAnswers = progressRecords.reduce( + (sum, record) => sum + (record.isCorrect ? 1 : 0), + 0, + ); + const accuracy = totalAttempts > 0 ? Math.round((correctAnswers / totalAttempts) * 100) : 0; - // Get category name - const category = await this.progressRepo - .createQueryBuilder('progress') - .leftJoinAndSelect('progress.category', 'category') - .where('progress.categoryId = :categoryId', { categoryId }) - .select('category.name') - .limit(1) - .getRawOne(); + const categoryName = progressRecords[0]?.category?.name || ''; return { categoryId, - categoryName: category?.category_name || '', + categoryName, totalAttempts, correctAnswers, accuracy, diff --git a/backend/src/progress/providers/get-overall-stats.provider.ts b/backend/src/progress/providers/get-overall-stats.provider.ts index 0c6d036..c84aa21 100644 --- a/backend/src/progress/providers/get-overall-stats.provider.ts +++ b/backend/src/progress/providers/get-overall-stats.provider.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { FindOptionsWhere, Repository } from 'typeorm'; import { UserProgress } from '../entities/progress.entity'; import { OverallStatsDto } from '../dtos/overall-stats.dto'; @@ -19,19 +19,13 @@ export class GetOverallStatsProvider { ) {} async getOverallStats(userId: string): Promise { - const result = await this.progressRepo - .createQueryBuilder('progress') - .select('COUNT(*)', 'totalAttempts') - .addSelect( - 'SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', - 'totalCorrect', - ) - .addSelect('SUM(progress.pointsEarned)', 'totalPointsEarned') - .addSelect('SUM(progress.timeSpent)', 'totalTimeSpent') - .where('progress.userId = :userId', { userId }) - .getRawOne(); - - if (!result) { + const where: FindOptionsWhere = { + userId, + }; + + const progressRecords = await this.progressRepo.find({ where }); + + if (progressRecords.length === 0) { return { totalAttempts: 0, totalCorrect: 0, @@ -41,8 +35,20 @@ export class GetOverallStatsProvider { }; } - const totalAttempts = parseInt(result.totalAttempts, 10) || 0; - const totalCorrect = parseInt(result.totalCorrect, 10) || 0; + const totalAttempts = progressRecords.length; + const totalCorrect = progressRecords.reduce( + (sum, record) => sum + (record.isCorrect ? 1 : 0), + 0, + ); + const totalPointsEarned = progressRecords.reduce( + (sum, record) => sum + record.pointsEarned, + 0, + ); + const totalTimeSpent = progressRecords.reduce( + (sum, record) => sum + record.timeSpent, + 0, + ); + const accuracy = totalAttempts > 0 ? Math.round((totalCorrect / totalAttempts) * 100) : 0; @@ -50,8 +56,8 @@ export class GetOverallStatsProvider { totalAttempts, totalCorrect, accuracy, - totalPointsEarned: parseInt(result.totalPointsEarned, 10) || 0, - totalTimeSpent: parseInt(result.totalTimeSpent, 10) || 0, + totalPointsEarned, + totalTimeSpent, }; } } From 1dadd50b316e247644ceac6afff3c14941b9dcaf Mon Sep 17 00:00:00 2001 From: lynn Date: Sat, 31 Jan 2026 07:22:40 +0100 Subject: [PATCH 4/6] provider history --- .../get-progress-history.provider.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/src/progress/providers/get-progress-history.provider.ts b/backend/src/progress/providers/get-progress-history.provider.ts index 60d4b72..5213110 100644 --- a/backend/src/progress/providers/get-progress-history.provider.ts +++ b/backend/src/progress/providers/get-progress-history.provider.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { FindOptionsWhere, Repository } from 'typeorm'; import { UserProgress } from '../entities/progress.entity'; -import { paginate } from '../../common/pagination/paginate'; @Injectable() export class GetProgressHistoryProvider { @@ -16,12 +15,25 @@ export class GetProgressHistoryProvider { page: number = 1, limit: number = 10, ) { - const qb = this.progressRepo - .createQueryBuilder('progress') - .leftJoinAndSelect('progress.puzzle', 'puzzle') - .where('progress.userId = :userId', { userId }) - .orderBy('progress.attemptedAt', 'DESC'); + const where: FindOptionsWhere = { + userId, + }; - return paginate(qb, page, limit); + const [data, total] = await this.progressRepo.findAndCount({ + where, + relations: ['puzzle'], + order: { attemptedAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + data, + meta: { + page, + limit, + total, + }, + }; } } From 2dece43665d8084c17f56fbcfee87ecc76085b9a Mon Sep 17 00:00:00 2001 From: lynn Date: Sat, 31 Jan 2026 07:23:27 +0100 Subject: [PATCH 5/6] provider history --- .../progress-calculation.provider.ts | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/backend/src/progress/providers/progress-calculation.provider.ts b/backend/src/progress/providers/progress-calculation.provider.ts index 40f4a64..61ee821 100644 --- a/backend/src/progress/providers/progress-calculation.provider.ts +++ b/backend/src/progress/providers/progress-calculation.provider.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { MoreThan, Repository } from 'typeorm'; +import { FindOptionsWhere, MoreThan, Repository } from 'typeorm'; import { Puzzle } from '../../puzzles/entities/puzzle.entity'; import { UserProgress } from '../entities/progress.entity'; import { SubmitAnswerDto } from '../dtos/submit-answer.dto'; @@ -245,20 +245,14 @@ export class ProgressCalculationProvider { * Gets user progress statistics for a category */ async getUserProgressStats(userId: string, categoryId: string) { - const stats = await this.userProgressRepository - .createQueryBuilder('progress') - .select('COUNT(*)', 'totalAttempts') - .addSelect( - 'SUM(CASE WHEN progress.isCorrect = true THEN 1 ELSE 0 END)', - 'correctAttempts', - ) - .addSelect('SUM(progress.pointsEarned)', 'totalPoints') - .addSelect('AVG(progress.timeSpent)', 'averageTimeSpent') - .where('progress.userId = :userId', { userId }) - .andWhere('progress.categoryId = :categoryId', { categoryId }) - .getRawOne(); - - if (!stats) { + const where: FindOptionsWhere = { + userId, + categoryId, + }; + + const progressRecords = await this.userProgressRepository.find({ where }); + + if (progressRecords.length === 0) { return { totalAttempts: 0, correctAttempts: 0, @@ -268,17 +262,31 @@ export class ProgressCalculationProvider { }; } + const totalAttempts = progressRecords.length; + const correctAttempts = progressRecords.reduce( + (sum, record) => sum + (record.isCorrect ? 1 : 0), + 0, + ); + const totalPoints = progressRecords.reduce( + (sum, record) => sum + record.pointsEarned, + 0, + ); + const totalTimeSpent = progressRecords.reduce( + (sum, record) => sum + record.timeSpent, + 0, + ); + const averageTimeSpent = + totalAttempts > 0 ? totalTimeSpent / totalAttempts : 0; + + const accuracy = + totalAttempts > 0 ? (correctAttempts / totalAttempts) * 100 : 0; + return { - totalAttempts: Number(stats?.totalAttempts) || 0, - correctAttempts: parseInt(stats?.correctAttempts || '0', 10), - totalPoints: parseInt(stats?.totalPoints || '0', 10), - averageTimeSpent: parseFloat(stats?.averageTimeSpent || '0'), - accuracy: - stats && Number(stats.totalAttempts) > 0 - ? (parseInt(stats.correctAttempts, 10) / - Number(stats.totalAttempts)) * - 100 - : 0, + totalAttempts, + correctAttempts, + totalPoints, + averageTimeSpent, + accuracy, }; } } From df42d2dafe879bb0f26316161910b49625e80368 Mon Sep 17 00:00:00 2001 From: lynn Date: Sat, 31 Jan 2026 07:25:19 +0100 Subject: [PATCH 6/6] getTodaysDailyQuest --- .../providers/getTodaysDailyQuest.provider.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/backend/src/quests/providers/getTodaysDailyQuest.provider.ts b/backend/src/quests/providers/getTodaysDailyQuest.provider.ts index 77910d5..c7374ef 100644 --- a/backend/src/quests/providers/getTodaysDailyQuest.provider.ts +++ b/backend/src/quests/providers/getTodaysDailyQuest.provider.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { FindOptionsWhere, In, Repository } from 'typeorm'; import { DailyQuest } from '../entities/daily-quest.entity'; import { DailyQuestPuzzle } from '../entities/daily-quest-puzzle.entity'; import { Puzzle } from '../../puzzles/entities/puzzle.entity'; @@ -153,15 +153,22 @@ export class GetTodaysDailyQuestProvider { categoryIds: string[], count: number, ): Promise { - const puzzles = await this.puzzleRepository - .createQueryBuilder('puzzle') - .where('puzzle.difficulty = :difficulty', { difficulty }) - .andWhere('puzzle.categoryId IN (:...categoryIds)', { categoryIds }) - .orderBy('RANDOM()') - .limit(count) - .getMany(); - - return puzzles; + const where: FindOptionsWhere = { + difficulty, + categoryId: In(categoryIds), + }; + + const puzzles = await this.puzzleRepository.find({ + where, + }); + + // Fisher-Yates shuffle for randomized order similar to ORDER BY RANDOM() + for (let i = puzzles.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [puzzles[i], puzzles[j]] = [puzzles[j], puzzles[i]]; + } + + return puzzles.slice(0, count); } private async buildQuestResponse(