Skip to content

Commit

Permalink
feat: refactor statistic maodule
Browse files Browse the repository at this point in the history
  • Loading branch information
wielopolski committed Nov 21, 2024
1 parent a2bb6e6 commit 22ca380
Show file tree
Hide file tree
Showing 10 changed files with 529 additions and 315 deletions.
1 change: 1 addition & 0 deletions apps/api/src/courses/courses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ export class CoursesService {

if (!course) throw new NotFoundException("Course not found");

// TODO: to remove and start use getLessonsDetails form lessonsRepository
const courseLessonList = await this.db
.select({
id: lessons.id,
Expand Down
90 changes: 89 additions & 1 deletion apps/api/src/lessons/repositories/lessons.repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable } from "@nestjs/common";
import { and, count, eq, inArray, sql } from "drizzle-orm";
import { and, count, desc, eq, inArray, isNotNull, sql } from "drizzle-orm";

import { DatabasePg, type UUIDType } from "src/common";
import { STATES } from "src/common/states";
Expand All @@ -22,6 +22,7 @@ import { LESSON_TYPE } from "../lesson.type";

import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import type * as schema from "src/storage/schema";

Check failure on line 24 in apps/api/src/lessons/repositories/lessons.repository.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups
import { LessonProgress } from "../schemas/lesson.types";

Check failure on line 25 in apps/api/src/lessons/repositories/lessons.repository.ts

View workflow job for this annotation

GitHub Actions / Lint

`../schemas/lesson.types` import should occur before type import of `drizzle-orm/postgres-js`

@Injectable()
export class LessonsRepository {
Expand Down Expand Up @@ -328,6 +329,78 @@ export class LessonsRepository {
return quizScore.quizScore;
}

async getLessonsDetails(userId: UUIDType, courseId: UUIDType, lessonId?: UUIDType) {
const conditions = [
eq(courseLessons.courseId, courseId),
eq(lessons.archived, false),
eq(lessons.state, STATES.published),
isNotNull(lessons.id),
isNotNull(lessons.title),
isNotNull(lessons.description),
isNotNull(lessons.imageUrl),
];
if (lessonId) conditions.push(eq(lessons.id, lessonId));

return await this.db
.select({
id: lessons.id,
title: lessons.title,
type: lessons.type,
isSubmitted: sql<boolean>`
EXISTS (
SELECT 1
FROM ${studentLessonsProgress}
WHERE ${studentLessonsProgress.lessonId} = ${lessons.id}
AND ${studentLessonsProgress.courseId} = ${courseId}
AND ${studentLessonsProgress.studentId} = ${userId}
AND ${studentLessonsProgress.quizCompleted}
)::BOOLEAN`,
description: sql<string>`${lessons.description}`,
imageUrl: sql<string>`${lessons.imageUrl}`,
itemsCount: sql<number>`
(SELECT COUNT(*)
FROM ${lessonItems}
WHERE ${lessonItems.lessonId} = ${lessons.id}
AND ${lessonItems.lessonItemType} != 'text_block')::INTEGER`,
itemsCompletedCount: sql<number>`
(SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lessons.id}
AND ${studentCompletedLessonItems.courseId} = ${courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId})::INTEGER`,
lessonProgress: sql<"completed" | "in_progress" | "not_started">`
(CASE
WHEN (
SELECT COUNT(*)
FROM ${lessonItems}
WHERE ${lessonItems.lessonId} = ${lessons.id}
AND ${lessonItems.lessonItemType} != 'text_block'
) = (
SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lessons.id}
AND ${studentCompletedLessonItems.courseId} = ${courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId}
)
THEN ${LessonProgress.completed}
WHEN (
SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lessons.id}
AND ${studentCompletedLessonItems.courseId} = ${courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId}
) > 0
THEN ${LessonProgress.inProgress}
ELSE ${LessonProgress.notStarted}
END)
`,
isFree: courseLessons.isFree,
})
.from(courseLessons)
.innerJoin(lessons, eq(courseLessons.lessonId, lessons.id))
.where(and(...conditions));
}

async completeQuiz(
courseId: UUIDType,
lessonId: UUIDType,
Expand Down Expand Up @@ -452,6 +525,21 @@ export class LessonsRepository {
.limit(1);
}

async getLastLessonItemForUser(userId: UUIDType) {
const [lastLessonItem] = await this.db
.select({
id: studentCompletedLessonItems.lessonItemId,
lessonId: studentCompletedLessonItems.lessonId,
courseId: studentCompletedLessonItems.courseId,
})
.from(studentCompletedLessonItems)
.where(eq(studentCompletedLessonItems.studentId, userId))
.orderBy(desc(studentCompletedLessonItems.updatedAt))
.limit(1);

return lastLessonItem;
}

async removeQuestionsAnswer(
courseId: UUIDType,
lessonId: UUIDType,
Expand Down
11 changes: 9 additions & 2 deletions apps/api/src/statistics/api/statistics.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { Controller, Get } from "@nestjs/common";

import { BaseResponse, UUIDType } from "src/common";
import { baseResponse, BaseResponse, UUIDType } from "src/common";
import { CurrentUser } from "src/common/decorators/user.decorator";

import { StatisticsService } from "../statistics.service";

Check failure on line 6 in apps/api/src/statistics/api/statistics.controller.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups
import { Validate } from "nestjs-typebox";

Check failure on line 7 in apps/api/src/statistics/api/statistics.controller.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups

Check failure on line 7 in apps/api/src/statistics/api/statistics.controller.ts

View workflow job for this annotation

GitHub Actions / Lint

`nestjs-typebox` import should occur before import of `src/common`
import { UserStats, UserStatsSchema } from "../schemas/userStats.schema";

Check failure on line 8 in apps/api/src/statistics/api/statistics.controller.ts

View workflow job for this annotation

GitHub Actions / Lint

`../schemas/userStats.schema` import should occur before import of `../statistics.service`

Check failure on line 8 in apps/api/src/statistics/api/statistics.controller.ts

View workflow job for this annotation

GitHub Actions / Lint

Import "UserStats" is only used as types

@Controller("statistics")
export class StatisticsController {
constructor(private statisticsService: StatisticsService) {}

@Get()
async getUserStatistics(@CurrentUser("userId") currentUserId: UUIDType) {
@Validate({
response: baseResponse(UserStatsSchema),
})
async getUserStatistics(
@CurrentUser("userId") currentUserId: UUIDType,
): Promise<BaseResponse<UserStats>> {
return new BaseResponse(await this.statisticsService.getUserStats(currentUserId));
}
}
8 changes: 6 additions & 2 deletions apps/api/src/statistics/handlers/statistics.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import { QuizCompletedEvent, UserActivityEvent, CourseStartedEvent } from "src/e
import { StatisticsRepository } from "../repositories/statistics.repository";

import type { IEventHandler } from "@nestjs/cqrs";

Check failure on line 9 in apps/api/src/statistics/handlers/statistics.handler.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups
import { StatisticsService } from "../statistics.service";

Check failure on line 10 in apps/api/src/statistics/handlers/statistics.handler.ts

View workflow job for this annotation

GitHub Actions / Lint

`../statistics.service` import should occur before type import of `@nestjs/cqrs`

type StatisticsEvent = QuizCompletedEvent | UserActivityEvent | CourseStartedEvent;

@Injectable()
@EventsHandler(QuizCompletedEvent, UserActivityEvent, CourseStartedEvent)
export class StatisticsHandler implements IEventHandler<QuizCompletedEvent | UserActivityEvent> {
constructor(private readonly statisticsRepository: StatisticsRepository) {}
constructor(
private readonly statisticsRepository: StatisticsRepository,
private readonly statisticsService: StatisticsService,
) {}

async handle(event: StatisticsEvent) {
try {
Expand Down Expand Up @@ -50,6 +54,6 @@ export class StatisticsHandler implements IEventHandler<QuizCompletedEvent | Use
}

private async handleUserActivity(event: UserActivityEvent) {
await this.statisticsRepository.updateUserActivity(event.userId);
await this.statisticsService.updateUserActivity(event.userId);
}
}
Loading

0 comments on commit 22ca380

Please sign in to comment.