From 356a28b1c89159c00c8932aa1e61b33b5a856932 Mon Sep 17 00:00:00 2001 From: wielopolski Date: Mon, 23 Dec 2024 14:50:39 +0100 Subject: [PATCH] feat: update lesson progress module --- apps/api/src/lesson/lesson.controller.ts | 71 +- apps/api/src/lesson/lesson.module.ts | 4 +- apps/api/src/lesson/lesson.schema.ts | 2 +- apps/api/src/lesson/lesson.type.ts | 2 +- .../lesson/services/adminLesson.service.ts | 2 +- .../api/src/lesson/services/lesson.service.ts | 49 +- apps/api/src/seed/e2e-data-seeds.ts | 2 +- apps/api/src/seed/nice-data-seeds.ts | 198 +- apps/api/src/seed/seed-helpers.ts | 4 +- apps/api/src/seed/seed.ts | 6 +- ..._chapter_id_to_student_lesson_progress.sql | 9 + .../migrations/meta/0004_snapshot.json | 1868 +++++++++++++++++ .../src/storage/migrations/meta/_journal.json | 7 + apps/api/src/storage/schema/index.ts | 5 +- .../studentLessonProgress.service.ts | 152 +- apps/web/app/api/generated-api.ts | 33 + 16 files changed, 2242 insertions(+), 172 deletions(-) create mode 100644 apps/api/src/storage/migrations/0004_add_chapter_id_to_student_lesson_progress.sql create mode 100644 apps/api/src/storage/migrations/meta/0004_snapshot.json diff --git a/apps/api/src/lesson/lesson.controller.ts b/apps/api/src/lesson/lesson.controller.ts index 8a1986d6..864c0715 100644 --- a/apps/api/src/lesson/lesson.controller.ts +++ b/apps/api/src/lesson/lesson.controller.ts @@ -18,21 +18,23 @@ import { CurrentUser } from "src/common/decorators/user.decorator"; import { RolesGuard } from "src/common/guards/roles.guard"; import { USER_ROLES } from "src/user/schemas/userRoles"; -import { AdminLessonService } from "./services/adminLesson.service"; import { CreateLessonBody, createLessonSchema, CreateQuizLessonBody, createQuizLessonSchema, - LessonShow, lessonShowSchema, UpdateLessonBody, updateLessonSchema, UpdateQuizLessonBody, updateQuizLessonSchema, } from "./lesson.schema"; +import { AdminLessonService } from "./services/adminLesson.service"; import { LessonService } from "./services/lesson.service"; +import type { + LessonShow} from "./lesson.schema"; + @Controller("lesson") @UseGuards(RolesGuard) export class LessonController { @@ -157,56 +159,21 @@ export class LessonController { }); } - // @Patch("course-lesson") - // @Roles(USER_ROLES.TEACHER, USER_ROLES.ADMIN) - // @Validate({ - // request: [ - // { - // type: "body", - // schema: Type.Object({ - // courseId: UUIDSchema, - // lessonId: UUIDSchema, - // isFree: Type.Boolean(), - // }), - // }, - // ], - // response: baseResponse(Type.Object({ isFree: Type.Boolean(), message: Type.String() })), - // }) - // async toggleLessonAsFree( - // @Body() body: { courseId: string; lessonId: string; isFree: boolean }, - // ): Promise> { - // const [toggledLesson] = await this.adminLessonsService.toggleLessonAsFree( - // body.courseId, - // body.lessonId, - // body.isFree, - // ); - // return new BaseResponse({ - // isFree: toggledLesson.isFree, - // message: body.isFree - // ? "Lesson toggled as free successfully" - // : "Lesson toggled as not free successfully", - // }); - // } - - // @Post("evaluation-quiz") - // @Roles(USER_ROLES.STUDENT) - // @Validate({ - // request: [ - // { type: "query", name: "courseId", schema: UUIDSchema }, - // { type: "query", name: "lessonId", schema: UUIDSchema }, - // ], - // response: baseResponse(Type.Object({ message: Type.String() })), - // }) - // async evaluationQuiz( - // @Query("courseId") courseId: string, - // @Query("lessonId") lessonId: string, - // @CurrentUser("userId") currentUserId: UUIDType, - // ): Promise> { - // await this.lessonsService.evaluationQuiz(courseId, lessonId, currentUserId); - // return new BaseResponse({ - // message: "Evaluation quiz successfully", - // }); - // } + // @Post("evaluation-quiz") + // @Roles(USER_ROLES.STUDENT) + // @Validate({ + // request: [{ type: "query", name: "lessonId", schema: UUIDSchema, required: true }], + // response: baseResponse(Type.Object({ message: Type.String() })), + // }) + // async evaluationQuiz( + // @Query("lessonId") lessonId: string, + // @CurrentUser("userId") currentUserId: UUIDType, + // ): Promise> { + // await this.lessonService.evaluationQuiz(lessonId, currentUserId); + // return new BaseResponse({ + // message: "Evaluation quiz successfully", + // }); + // } // @Delete("clear-quiz-progress") // @Roles(USER_ROLES.STUDENT) diff --git a/apps/api/src/lesson/lesson.module.ts b/apps/api/src/lesson/lesson.module.ts index faf189ee..126b294b 100644 --- a/apps/api/src/lesson/lesson.module.ts +++ b/apps/api/src/lesson/lesson.module.ts @@ -2,10 +2,10 @@ import { Module } from "@nestjs/common"; import { FileModule } from "src/file/files.module"; -import { AdminLessonRepository } from "./repositories/adminLesson.repository"; -import { AdminLessonService } from "./services/adminLesson.service"; import { LessonController } from "./lesson.controller"; +import { AdminLessonRepository } from "./repositories/adminLesson.repository"; import { LessonRepository } from "./repositories/lesson.repository"; +import { AdminLessonService } from "./services/adminLesson.service"; import { LessonService } from "./services/lesson.service"; @Module({ diff --git a/apps/api/src/lesson/lesson.schema.ts b/apps/api/src/lesson/lesson.schema.ts index 1e81ca0f..59f4aa12 100644 --- a/apps/api/src/lesson/lesson.schema.ts +++ b/apps/api/src/lesson/lesson.schema.ts @@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox"; import { UUIDSchema } from "src/common"; -import { LESSON_TYPES, PhotoQuestionType, QuestionType } from "./lesson.type"; +import { PhotoQuestionType, QuestionType } from "./lesson.type"; import type { Static } from "@sinclair/typebox"; diff --git a/apps/api/src/lesson/lesson.type.ts b/apps/api/src/lesson/lesson.type.ts index 5f2e7cc2..3725b221 100644 --- a/apps/api/src/lesson/lesson.type.ts +++ b/apps/api/src/lesson/lesson.type.ts @@ -1,5 +1,5 @@ export const LESSON_TYPES = { - TEXT_BLOCK: "text_block", + TEXT: "text", FILE: "file", PRESENTATION: "presentation", VIDEO: "video", diff --git a/apps/api/src/lesson/services/adminLesson.service.ts b/apps/api/src/lesson/services/adminLesson.service.ts index 4e4f8e51..04c5401b 100644 --- a/apps/api/src/lesson/services/adminLesson.service.ts +++ b/apps/api/src/lesson/services/adminLesson.service.ts @@ -4,9 +4,9 @@ import { eq, gte, inArray, lte, sql } from "drizzle-orm"; import { DatabasePg } from "src/common"; import { lessons, questionAnswerOptions, questions } from "src/storage/schema"; +import { LESSON_TYPES } from "../lesson.type"; import { AdminLessonRepository } from "../repositories/adminLesson.repository"; import { LessonRepository } from "../repositories/lesson.repository"; -import { LESSON_TYPES } from "../lesson.type"; import type { CreateLessonBody, diff --git a/apps/api/src/lesson/services/lesson.service.ts b/apps/api/src/lesson/services/lesson.service.ts index a3ce8798..ad1baffe 100644 --- a/apps/api/src/lesson/services/lesson.service.ts +++ b/apps/api/src/lesson/services/lesson.service.ts @@ -1,23 +1,30 @@ import { Inject, Injectable, NotFoundException } from "@nestjs/common"; -import { EventBus } from "@nestjs/cqrs"; -import { eq, sql } from "drizzle-orm"; +import { and, eq, sql } from "drizzle-orm"; import { DatabasePg } from "src/common"; +import { FileService } from "src/file/file.service"; +import { + chapters, + lessons, + questionAnswerOptions, + questions, + studentCourses, +} from "src/storage/schema"; + +import { LESSON_TYPES } from "../lesson.type"; + +import type { LessonShow, OptionBody, QuestionBody } from "../lesson.schema"; +import type { PhotoQuestionType, QuestionType } from "../lesson.type"; import type { UUIDType } from "src/common"; -import { lessons, questionAnswerOptions, questions } from "src/storage/schema"; -import { LESSON_TYPES, PhotoQuestionType, QuestionType } from "../lesson.type"; -import { FileService } from "src/file/file.service"; -import { LessonShow, OptionBody, QuestionBody } from "../lesson.schema"; -import { QuestionTypes } from "src/questions/schema/questions.types"; @Injectable() export class LessonService { constructor( @Inject("DB") private readonly db: DatabasePg, private readonly fileService: FileService, // TODO: add event bus - ) // private readonly eventBus: EventBus, - {} + // private readonly eventBus: EventBus, + ) {} async getLessonById(id: UUIDType): Promise { const [lesson] = await this.db @@ -34,7 +41,7 @@ export class LessonService { if (!lesson) throw new NotFoundException("Lesson not found"); - if (lesson.type === LESSON_TYPES.TEXT_BLOCK && !lesson.fileUrl) return lesson; + if (lesson.type === LESSON_TYPES.TEXT && !lesson.fileUrl) return lesson; if (lesson.type !== LESSON_TYPES.QUIZ) { if (!lesson.fileUrl) throw new NotFoundException("Lesson file not found"); @@ -97,12 +104,8 @@ export class LessonService { return { ...lesson, quizDetails }; } - // async evaluationQuiz(courseId: UUIDType, lessonId: UUIDType, userId: UUIDType) { - // const [accessCourseLessons] = await this.chapterRepository.checkLessonAssignment( - // courseId, - // lessonId, - // userId, - // ); + // async evaluationQuiz(lessonId: UUIDType, userId: UUIDType) { + // const [accessCourseLessons] = await this.checkLessonAssignment(lessonId, userId); // if (!accessCourseLessons.isAssigned && !accessCourseLessons.isFree) // throw new UnauthorizedException("You don't have assignment to this lesson"); @@ -140,6 +143,20 @@ export class LessonService { // return true; // } + async checkLessonAssignment(id: UUIDType, userId: UUIDType) { + return this.db + .select({ + isAssigned: sql`CASE WHEN ${studentCourses.id} IS NOT NULL THEN TRUE ELSE FALSE END`, + }) + .from(lessons) + .leftJoin(chapters, eq(lessons.chapterId, chapters.id)) + .leftJoin( + studentCourses, + and(eq(studentCourses.courseId, chapters.courseId), eq(studentCourses.studentId, userId)), + ) + .where(and(eq(chapters.isPublished, true), eq(lessons.id, id))); + } + // private async evaluationsQuestions(courseId: UUIDType, lessonId: UUIDType, userId: UUIDType) { // const lesson = await this.chapterRepository.getLessonForUser(courseId, lessonId, userId); // const questionLessonItems = await this.getLessonQuestionsToEvaluation( diff --git a/apps/api/src/seed/e2e-data-seeds.ts b/apps/api/src/seed/e2e-data-seeds.ts index 1d360e07..4bfc2c67 100644 --- a/apps/api/src/seed/e2e-data-seeds.ts +++ b/apps/api/src/seed/e2e-data-seeds.ts @@ -18,7 +18,7 @@ export const e2eCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "E2E Testing Text Block", description: "E2E Testing Text Block Body", displayOrder: 1, diff --git a/apps/api/src/seed/nice-data-seeds.ts b/apps/api/src/seed/nice-data-seeds.ts index f827b4f1..84eb1c6e 100644 --- a/apps/api/src/seed/nice-data-seeds.ts +++ b/apps/api/src/seed/nice-data-seeds.ts @@ -21,7 +21,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Introduction to HTML", description: "HTML (HyperText Markup Language) is the standard language used to create the structure of web pages. In this lesson, you'll explore basic HTML elements and how they are used to build the framework of a website.", @@ -486,7 +486,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Introduction to HTML", description: "HTML (HyperText Markup Language) is the standard language used to create the structure of web pages. In this lesson, you'll explore basic HTML elements and how they are used to build the framework of a website.", @@ -571,7 +571,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Introduction to Java for Android", description: "Java is the primary language used for Android app development. In this lesson, you'll learn about Java syntax, data types, and object-oriented programming principles that form the foundation of Android development.", @@ -754,7 +754,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Introduction to Kotlin for Android", description: "Kotlin is a modern, concise language used for Android development. In this lesson, you'll learn about Kotlin syntax and basic concepts for creating Android apps.", @@ -794,7 +794,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Setting Up Your Android Studio Environment", description: "Learn how to configure Android Studio for Kotlin development and create your first Android project.", @@ -845,7 +845,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Introduction to Arithmetic", description: "Arithmetic is the foundation of mathematics. In this lesson, you'll learn about numbers, basic operations, and their properties.", @@ -900,7 +900,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Understanding Geometry", description: "Geometry involves the study of shapes, sizes, and the properties of space. In this lesson, you'll learn about basic geometric figures and their properties.", @@ -940,7 +940,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Getting Started with Algebra", description: "Algebra helps us solve problems by finding unknown values. In this lesson, you'll learn about variables, expressions, and simple equations.", @@ -1042,14 +1042,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Introduction to English Grammar", description: "Learn the essential grammar rules that form the backbone of English communication, covering nouns, verbs, adjectives, and more.", displayOrder: 1, }, { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Sentence Structure Basics", description: "Explore how sentences are structured, including subject-verb agreement and word order in affirmative, negative, and question forms.", @@ -1101,14 +1101,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Common English Words and Phrases", description: "A beginner-friendly list of common English words and phrases you can use in daily conversations.", displayOrder: 1, }, { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Synonyms and Antonyms", description: "Learn about the importance of synonyms and antonyms in expanding your vocabulary and making your speech more varied.", @@ -1162,14 +1162,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Essential Pronunciation Tips", description: "Learn how to pronounce English words correctly and improve your accent with practical tips and exercises.", displayOrder: 1, }, { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Common Pronunciation Mistakes", description: "Identify and work on common pronunciation challenges for non-native English speakers.", @@ -1310,14 +1310,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Complex Sentences and Their Use", description: "Learn how to form and use complex sentences to convey more detailed thoughts and ideas effectively.", displayOrder: 1, }, { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Relative Clauses and Modifiers", description: "A deep dive into relative clauses and modifiers, which help to add extra information to sentences.", @@ -1361,14 +1361,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Academic Vocabulary and Its Application", description: "Master vocabulary words commonly used in academic papers, essays, and formal discussions.", displayOrder: 1, }, { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Using Formal Language in Communication", description: "Learn how to adjust your language for formal situations, such as presentations or professional meetings.", @@ -1420,14 +1420,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, lessons: [ { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Understanding Idioms in Context", description: "Learn how idiomatic expressions are used in everyday conversations to sound more natural and fluent.", displayOrder: 1, }, { - type: LESSON_TYPES.TEXT_BLOCK, + type: LESSON_TYPES.TEXT, title: "Common Idioms and Their Meanings", description: "A list of frequently used idioms, their meanings, and examples of how to use them.", @@ -1464,4 +1464,162 @@ export const niceCourses: NiceCourseData[] = [ }, ], }, + { + title: "Artificial Intelligence in Business: Fundamentals", + description: + "This beginner-friendly course introduces the basics of AI in business. Learn about key concepts, terminologies, and how AI is applied to improve efficiency, automate processes, and enhance decision-making in various industries. By the end, you'll understand AI's potential to transform your business.", + isPublished: true, + priceInCents: 12900, + category: "Artificial Intelligence", + thumbnailS3Key: faker.image.urlPicsumPhotos(), + chapters: [ + { + title: "Understanding AI Basics", + isPublished: true, + isFreemium: true, + displayOrder: 1, + lessons: [ + { + type: LESSON_TYPES.VIDEO, + title: "What is Artificial Intelligence? An Introductory Overview", + description: + "A comprehensive video introduction to the concept of Artificial Intelligence, its history, and its significance in business.", + displayOrder: 1, + }, + { + type: LESSON_TYPES.TEXT, + title: "Key Concepts and Terminologies in AI", + description: + "Learn the foundational terminologies of AI, including Machine Learning, Neural Networks, NLP, and more.", + displayOrder: 2, + }, + { + type: LESSON_TYPES.PRESENTATION, + title: "AI Applications Across Industries", + description: + "A presentation exploring real-world AI applications in sectors like healthcare, finance, and retail.", + displayOrder: 3, + }, + { + type: LESSON_TYPES.QUIZ, + title: "AI Quiz: Primary Goal of AI in Business", + description: + "Test your understanding of the fundamental goal of AI in business applications.", + displayOrder: 4, + questions: [ + { + type: QuestionType.SingleChoice, + title: "What is the primary goal of AI in business?", + options: [ + { optionText: "Replace human workers", isCorrect: false, position: 0 }, + { optionText: "Fully automate all tasks", isCorrect: false, position: 1 }, + { optionText: "Improve decision-making", isCorrect: true, position: 2 }, + { optionText: "Eliminate all business costs", isCorrect: false, position: 3 }, + ], + }, + ], + }, + { + type: LESSON_TYPES.QUIZ, + title: "AI Quiz: Applications of AI", + description: "Identify common AI applications in various business domains.", + displayOrder: 5, + questions: [ + { + type: QuestionType.MultipleChoice, + title: + "Which of the following are applications of AI in business? (Select all that apply)", + options: [ + { optionText: "Customer service chatbots", isCorrect: true, position: 0 }, + { optionText: "Predictive analytics", isCorrect: true, position: 1 }, + { optionText: "Supply chain optimization", isCorrect: true, position: 2 }, + { optionText: "Space exploration tools", isCorrect: false, position: 3 }, + ], + }, + ], + }, + { + type: LESSON_TYPES.QUIZ, + title: "AI Quiz: Can AI Function Without Data?", + description: "Test your understanding of AI's reliance on data.", + displayOrder: 6, + questions: [ + { + type: QuestionType.TrueOrFalse, + title: "AI can function without any data input from humans.", + options: [ + { optionText: "True", isCorrect: false, position: 0 }, + { optionText: "False", isCorrect: true, position: 1 }, + ], + }, + ], + }, + { + type: LESSON_TYPES.QUIZ, + title: "Photo Identification: AI Solutions", + description: "Identify the AI-driven solution from the provided images.", + displayOrder: 7, + questions: [ + { + type: QuestionType.PhotoQuestion, + title: "Which image represents an AI-driven chatbot?", + options: [ + { optionText: "Image 1 (Chatbot Interface)", isCorrect: true, position: 0 }, + { optionText: "Image 2 (Calculator)", isCorrect: false, position: 1 }, + { optionText: "Image 3 (Spreadsheet)", isCorrect: false, position: 2 }, + ], + }, + ], + }, + { + type: LESSON_TYPES.QUIZ, + title: "AI Fill in the Blanks", + description: "Complete the sentences with the correct AI-related terms.", + displayOrder: 8, + questions: [ + { + type: QuestionType.FillInTheBlanks, + title: + "Complete the blanks: Artificial [word] refers to the ability of machines to mimic [word] intelligence.", + description: + "

Complete the blanks: Artificial Intelligence refers to the ability of machines to mimic Human intelligence.

", + options: [ + { optionText: "Intelligence", isCorrect: true, position: 0 }, + { optionText: "Human", isCorrect: true, position: 1 }, + { optionText: "Automation", isCorrect: false, position: 2 }, + { optionText: "Learning", isCorrect: false, position: 3 }, + { optionText: "Animal", isCorrect: false, position: 4 }, + ], + }, + ], + }, + { + type: LESSON_TYPES.QUIZ, + title: "Brief Response: Why Businesses Adopt AI", + description: "Explain in one sentence why businesses adopt AI.", + displayOrder: 9, + questions: [ + { + type: QuestionType.BriefResponse, + title: "In one sentence, explain why businesses are adopting AI.", + }, + ], + }, + { + type: LESSON_TYPES.QUIZ, + title: "Detailed Response: AI's Role in an Industry", + description: "Describe how AI can improve decision-making in a specific industry.", + displayOrder: 10, + questions: [ + { + type: QuestionType.DetailedResponse, + title: + "Describe how AI can improve decision-making in a specific industry of your choice.", + }, + ], + }, + ], + }, + ], + }, ]; diff --git a/apps/api/src/seed/seed-helpers.ts b/apps/api/src/seed/seed-helpers.ts index 910c1dcf..514ca02c 100644 --- a/apps/api/src/seed/seed-helpers.ts +++ b/apps/api/src/seed/seed-helpers.ts @@ -95,8 +95,8 @@ export async function createNiceCourses( lessonData.type === LESSON_TYPES.PRESENTATION ? "pptx" : lessonData.type === LESSON_TYPES.VIDEO - ? "mp4" - : null, + ? "mp4" + : null, chapterId: chapter.id, createdAt: createdAt, updatedAt: createdAt, diff --git a/apps/api/src/seed/seed.ts b/apps/api/src/seed/seed.ts index e9bc92ea..7e93b421 100644 --- a/apps/api/src/seed/seed.ts +++ b/apps/api/src/seed/seed.ts @@ -146,6 +146,7 @@ async function createLessonProgress(userId: string) { const courseLessonsList = await db .select({ lessonId: sql`${lessons.id}`, + chapterId: sql`${chapters.id}`, createdAt: sql`${courses.createdAt}`, lessonType: sql`${lessons.type}`, }) @@ -156,11 +157,10 @@ async function createLessonProgress(userId: string) { .where(eq(studentCourses.studentId, userId)); const lessonProgressList = courseLessonsList.map((courseLesson) => { - const lessonId = courseLesson.lessonId; - return { - lessonId, studentId: userId, + lessonId: courseLesson.lessonId, + chapterId: courseLesson.chapterId, createdAt: courseLesson.createdAt, updatedAt: courseLesson.createdAt, quizScore: courseLesson.lessonType === LESSON_TYPES.QUIZ ? 0 : null, diff --git a/apps/api/src/storage/migrations/0004_add_chapter_id_to_student_lesson_progress.sql b/apps/api/src/storage/migrations/0004_add_chapter_id_to_student_lesson_progress.sql new file mode 100644 index 00000000..05da5520 --- /dev/null +++ b/apps/api/src/storage/migrations/0004_add_chapter_id_to_student_lesson_progress.sql @@ -0,0 +1,9 @@ +ALTER TABLE "student_lesson_progress" DROP CONSTRAINT "student_lesson_progress_student_id_lesson_id_unique";--> statement-breakpoint +ALTER TABLE "student_lesson_progress" ADD COLUMN "chapter_id" uuid;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "student_lesson_progress" ADD CONSTRAINT "student_lesson_progress_chapter_id_chapters_id_fk" FOREIGN KEY ("chapter_id") REFERENCES "public"."chapters"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +ALTER TABLE "student_lesson_progress" ADD CONSTRAINT "student_lesson_progress_student_id_lesson_id_chapter_id_unique" UNIQUE("student_id","lesson_id","chapter_id"); \ No newline at end of file diff --git a/apps/api/src/storage/migrations/meta/0004_snapshot.json b/apps/api/src/storage/migrations/meta/0004_snapshot.json new file mode 100644 index 00000000..e436353a --- /dev/null +++ b/apps/api/src/storage/migrations/meta/0004_snapshot.json @@ -0,0 +1,1868 @@ +{ + "id": "4a61ac2b-fd8b-4d94-a632-e4d19f0c32c8", + "prevId": "a69a4ab9-0250-4505-b38f-f18887d2b8e8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "categories_title_unique": { + "name": "categories_title_unique", + "nullsNotDistinct": false, + "columns": [ + "title" + ] + } + } + }, + "public.chapters": { + "name": "chapters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title": { + "name": "title", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_freemium": { + "name": "is_freemium", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lesson_count": { + "name": "lesson_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "chapters_course_id_courses_id_fk": { + "name": "chapters_course_id_courses_id_fk", + "tableFrom": "chapters", + "tableTo": "courses", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chapters_author_id_users_id_fk": { + "name": "chapters_author_id_users_id_fk", + "tableFrom": "chapters", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.course_students_stats": { + "name": "course_students_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "month": { + "name": "month", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "new_students_count": { + "name": "new_students_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "course_students_stats_course_id_courses_id_fk": { + "name": "course_students_stats_course_id_courses_id_fk", + "tableFrom": "course_students_stats", + "tableTo": "courses", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "course_students_stats_author_id_users_id_fk": { + "name": "course_students_stats_author_id_users_id_fk", + "tableFrom": "course_students_stats", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "course_students_stats_course_id_month_year_unique": { + "name": "course_students_stats_course_id_month_year_unique", + "nullsNotDistinct": false, + "columns": [ + "course_id", + "month", + "year" + ] + } + } + }, + "public.courses": { + "name": "courses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title": { + "name": "title", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "thumbnail_s3_key": { + "name": "thumbnail_s3_key", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "price_in_cents": { + "name": "price_in_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "currency": { + "name": "currency", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "chapter_count": { + "name": "chapter_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_scorm": { + "name": "is_scorm", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "courses_author_id_users_id_fk": { + "name": "courses_author_id_users_id_fk", + "tableFrom": "courses", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "courses_category_id_categories_id_fk": { + "name": "courses_category_id_categories_id_fk", + "tableFrom": "courses", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.courses_summary_stats": { + "name": "courses_summary_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "free_purchased_count": { + "name": "free_purchased_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "paid_purchased_count": { + "name": "paid_purchased_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "paid_purchased_after_freemium_count": { + "name": "paid_purchased_after_freemium_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_freemium_student_count": { + "name": "completed_freemium_student_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_course_student_count": { + "name": "completed_course_student_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "courses_summary_stats_course_id_courses_id_fk": { + "name": "courses_summary_stats_course_id_courses_id_fk", + "tableFrom": "courses_summary_stats", + "tableTo": "courses", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "courses_summary_stats_author_id_users_id_fk": { + "name": "courses_summary_stats_author_id_users_id_fk", + "tableFrom": "courses_summary_stats", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "courses_summary_stats_course_id_unique": { + "name": "courses_summary_stats_course_id_unique", + "nullsNotDistinct": false, + "columns": [ + "course_id" + ] + } + } + }, + "public.create_tokens": { + "name": "create_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "create_token": { + "name": "create_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiry_date": { + "name": "expiry_date", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "create_tokens_user_id_users_id_fk": { + "name": "create_tokens_user_id_users_id_fk", + "tableFrom": "create_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "credentials_user_id_users_id_fk": { + "name": "credentials_user_id_users_id_fk", + "tableFrom": "credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.lessons": { + "name": "lessons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "chapter_id": { + "name": "chapter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_s3_key": { + "name": "file_s3_key", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "lessons_chapter_id_chapters_id_fk": { + "name": "lessons_chapter_id_chapters_id_fk", + "tableFrom": "lessons", + "tableTo": "chapters", + "columnsFrom": [ + "chapter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.question_answer_options": { + "name": "question_answer_options", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "option_text": { + "name": "option_text", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "is_correct": { + "name": "is_correct", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "question_answer_options_question_id_questions_id_fk": { + "name": "question_answer_options_question_id_questions_id_fk", + "tableFrom": "question_answer_options", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.questions": { + "name": "questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "lesson_id": { + "name": "lesson_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "photo_s3_key": { + "name": "photo_s3_key", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "photo_question_type": { + "name": "photo_question_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "solution_explanation": { + "name": "solution_explanation", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "questions_lesson_id_lessons_id_fk": { + "name": "questions_lesson_id_lessons_id_fk", + "tableFrom": "questions", + "tableTo": "lessons", + "columnsFrom": [ + "lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "questions_author_id_users_id_fk": { + "name": "questions_author_id_users_id_fk", + "tableFrom": "questions", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.quiz_attempts": { + "name": "quiz_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "lesson_id": { + "name": "lesson_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "correct_answers": { + "name": "correct_answers", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "wrong_answers": { + "name": "wrong_answers", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "quiz_attempts_user_id_users_id_fk": { + "name": "quiz_attempts_user_id_users_id_fk", + "tableFrom": "quiz_attempts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "quiz_attempts_course_id_courses_id_fk": { + "name": "quiz_attempts_course_id_courses_id_fk", + "tableFrom": "quiz_attempts", + "tableTo": "courses", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "quiz_attempts_lesson_id_lessons_id_fk": { + "name": "quiz_attempts_lesson_id_lessons_id_fk", + "tableFrom": "quiz_attempts", + "tableTo": "lessons", + "columnsFrom": [ + "lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.reset_tokens": { + "name": "reset_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reset_token": { + "name": "reset_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiry_date": { + "name": "expiry_date", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reset_tokens_user_id_users_id_fk": { + "name": "reset_tokens_user_id_users_id_fk", + "tableFrom": "reset_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.scorm_files": { + "name": "scorm_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_key_path": { + "name": "s3_key_path", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.scorm_metadata": { + "name": "scorm_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entry_point": { + "name": "entry_point", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_key": { + "name": "s3_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "scorm_metadata_course_id_courses_id_fk": { + "name": "scorm_metadata_course_id_courses_id_fk", + "tableFrom": "scorm_metadata", + "tableTo": "courses", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scorm_metadata_file_id_scorm_files_id_fk": { + "name": "scorm_metadata_file_id_scorm_files_id_fk", + "tableFrom": "scorm_metadata", + "tableTo": "scorm_files", + "columnsFrom": [ + "file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.student_chapter_progress": { + "name": "student_chapter_progress", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "student_id": { + "name": "student_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chapter_id": { + "name": "chapter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "completed_lesson_count": { + "name": "completed_lesson_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_as_freemium": { + "name": "completed_as_freemium", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "student_chapter_progress_student_id_users_id_fk": { + "name": "student_chapter_progress_student_id_users_id_fk", + "tableFrom": "student_chapter_progress", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "student_chapter_progress_course_id_courses_id_fk": { + "name": "student_chapter_progress_course_id_courses_id_fk", + "tableFrom": "student_chapter_progress", + "tableTo": "courses", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "student_chapter_progress_chapter_id_chapters_id_fk": { + "name": "student_chapter_progress_chapter_id_chapters_id_fk", + "tableFrom": "student_chapter_progress", + "tableTo": "chapters", + "columnsFrom": [ + "chapter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "student_chapter_progress_student_id_course_id_chapter_id_unique": { + "name": "student_chapter_progress_student_id_course_id_chapter_id_unique", + "nullsNotDistinct": false, + "columns": [ + "student_id", + "course_id", + "chapter_id" + ] + } + } + }, + "public.student_courses": { + "name": "student_courses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "student_id": { + "name": "student_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "progress": { + "name": "progress", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "finished_chapter_count": { + "name": "finished_chapter_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "student_courses_student_id_users_id_fk": { + "name": "student_courses_student_id_users_id_fk", + "tableFrom": "student_courses", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "student_courses_course_id_courses_id_fk": { + "name": "student_courses_course_id_courses_id_fk", + "tableFrom": "student_courses", + "tableTo": "courses", + "columnsFrom": [ + "course_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "student_courses_student_id_course_id_unique": { + "name": "student_courses_student_id_course_id_unique", + "nullsNotDistinct": false, + "columns": [ + "student_id", + "course_id" + ] + } + } + }, + "public.student_lesson_progress": { + "name": "student_lesson_progress", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "student_id": { + "name": "student_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chapter_id": { + "name": "chapter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lesson_id": { + "name": "lesson_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "completed_question_count": { + "name": "completed_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "quiz_score": { + "name": "quiz_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "student_lesson_progress_student_id_users_id_fk": { + "name": "student_lesson_progress_student_id_users_id_fk", + "tableFrom": "student_lesson_progress", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "student_lesson_progress_chapter_id_chapters_id_fk": { + "name": "student_lesson_progress_chapter_id_chapters_id_fk", + "tableFrom": "student_lesson_progress", + "tableTo": "chapters", + "columnsFrom": [ + "chapter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "student_lesson_progress_lesson_id_lessons_id_fk": { + "name": "student_lesson_progress_lesson_id_lessons_id_fk", + "tableFrom": "student_lesson_progress", + "tableTo": "lessons", + "columnsFrom": [ + "lesson_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "student_lesson_progress_student_id_lesson_id_chapter_id_unique": { + "name": "student_lesson_progress_student_id_lesson_id_chapter_id_unique", + "nullsNotDistinct": false, + "columns": [ + "student_id", + "lesson_id", + "chapter_id" + ] + } + } + }, + "public.student_question_answers": { + "name": "student_question_answers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "student_id": { + "name": "student_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "is_correct": { + "name": "is_correct", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "student_question_answers_question_id_questions_id_fk": { + "name": "student_question_answers_question_id_questions_id_fk", + "tableFrom": "student_question_answers", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "student_question_answers_student_id_users_id_fk": { + "name": "student_question_answers_student_id_users_id_fk", + "tableFrom": "student_question_answers", + "tableTo": "users", + "columnsFrom": [ + "student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "student_question_answers_question_id_student_id_unique": { + "name": "student_question_answers_question_id_student_id_unique", + "nullsNotDistinct": false, + "columns": [ + "question_id", + "student_id" + ] + } + } + }, + "public.user_details": { + "name": "user_details", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_phone_number": { + "name": "contact_phone_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_details_user_id_users_id_fk": { + "name": "user_details_user_id_users_id_fk", + "tableFrom": "user_details", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_details_user_id_unique": { + "name": "user_details_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + } + }, + "public.user_statistics": { + "name": "user_statistics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_streak": { + "name": "current_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "longest_streak": { + "name": "longest_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_activity_date": { + "name": "last_activity_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "activity_history": { + "name": "activity_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_statistics_user_id_users_id_fk": { + "name": "user_statistics_user_id_users_id_fk", + "tableFrom": "user_statistics", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_statistics_user_id_unique": { + "name": "user_statistics_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + } + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'student'" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/storage/migrations/meta/_journal.json b/apps/api/src/storage/migrations/meta/_journal.json index 392102d4..00e47a21 100644 --- a/apps/api/src/storage/migrations/meta/_journal.json +++ b/apps/api/src/storage/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1734419608219, "tag": "0003_add_question_missing_columns", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1734960126182, + "tag": "0004_add_chapter_id_to_student_lesson_progress", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/storage/schema/index.ts b/apps/api/src/storage/schema/index.ts index aca4e395..6ea34a74 100644 --- a/apps/api/src/storage/schema/index.ts +++ b/apps/api/src/storage/schema/index.ts @@ -244,6 +244,9 @@ export const studentLessonProgress = pgTable( studentId: uuid("student_id") .references(() => users.id, { onDelete: "set null" }) .notNull(), + chapterId: uuid("chapter_id").references(() => chapters.id, { onDelete: "cascade" }), + // TODO: add notNull after deploy + // .notNull(), lessonId: uuid("lesson_id") .references(() => lessons.id, { onDelete: "cascade" }) .notNull(), @@ -256,7 +259,7 @@ export const studentLessonProgress = pgTable( }), }, (table) => ({ - unq: unique().on(table.studentId, table.lessonId), + unq: unique().on(table.studentId, table.lessonId, table.chapterId), }), ); diff --git a/apps/api/src/studentLessonProgress/studentLessonProgress.service.ts b/apps/api/src/studentLessonProgress/studentLessonProgress.service.ts index fad7a177..a9d214ed 100644 --- a/apps/api/src/studentLessonProgress/studentLessonProgress.service.ts +++ b/apps/api/src/studentLessonProgress/studentLessonProgress.service.ts @@ -1,11 +1,11 @@ -import { ConflictException, Inject, Injectable, NotFoundException } from "@nestjs/common"; +import { Inject, Injectable, NotFoundException } from "@nestjs/common"; import { and, eq, isNotNull, sql } from "drizzle-orm"; -import { LESSON_ITEM_TYPE } from "src/chapter/chapter.type"; import { DatabasePg } from "src/common"; import { StatisticsRepository } from "src/statistics/repositories/statistics.repository"; import { chapters, + courses, lessons, studentChapterProgress, studentCourses, @@ -14,6 +14,8 @@ import { import { PROGRESS_STATUSES } from "src/utils/types/progress.type"; import type { UUIDType } from "src/common"; +import type { ProgressStatus } from "src/utils/types/progress.type"; + @Injectable() export class StudentLessonProgressService { @@ -28,45 +30,84 @@ export class StudentLessonProgressService { id: lessons.id, type: lessons.type, chapterId: chapters.id, + chapterLessonCount: chapters.lessonCount, courseId: chapters.courseId, }) .from(lessons) .leftJoin(chapters, eq(chapters.id, lessons.chapterId)) .where(and(eq(lessons.id, id))); - if (!lesson || lesson.chapterId === null || lesson.courseId === null) { + if (!lesson || !lesson.chapterId || !lesson.courseId || !lesson.chapterLessonCount) { throw new NotFoundException(`Lesson with id ${id} not found`); } - if (lesson.type === "text_block") { - throw new ConflictException("Text block is not completable"); - } + const [createdLessonProgress] = await this.db + .insert(studentLessonProgress) + .values({ studentId, lessonId: lesson.id, completedAt: sql`now()` }) + .onConflictDoNothing() + .returning(); + + if (!createdLessonProgress) return; + + await this.updateChapterProgress(lesson.chapterId, studentId, lesson.chapterLessonCount); + + await this.checkCourseIsCompletedForUser(lesson.courseId, studentId); + } - const [existingRecord] = await this.db - .select() + private async updateChapterProgress( + chapterId: UUIDType, + studentId: UUIDType, + lessonCount: number, + ) { + const [completedLessonCount] = await this.db + .select({ count: sql`count(*)` }) .from(studentLessonProgress) .where( and( - eq(studentLessonProgress.lessonId, lesson.id), + eq(studentLessonProgress.chapterId, chapterId), eq(studentLessonProgress.studentId, studentId), + isNotNull(studentLessonProgress.completedAt), ), ); - if (existingRecord) return; - - await this.db.insert(studentLessonProgress).values({ studentId, lessonId: lesson.id }); + if (completedLessonCount.count === lessonCount) { + return await this.db + .update(studentChapterProgress) + .set({ + completedLessonCount: completedLessonCount.count, + completedAt: sql`now()`, + }) + .where( + and( + eq(studentChapterProgress.chapterId, chapterId), + eq(studentChapterProgress.studentId, studentId), + ), + ) + .returning(); + } - await this.checkCourseIsCompletedForUser(lesson.courseId, studentId); + return await this.db + .update(studentChapterProgress) + .set({ + completedLessonCount: completedLessonCount.count, + }) + .where( + and( + eq(studentChapterProgress.chapterId, chapterId), + eq(studentChapterProgress.studentId, studentId), + ), + ) + .returning(); } private async checkCourseIsCompletedForUser(courseId: UUIDType, studentId: UUIDType) { - const courseCompleted = await this.getCourseCompletionStatus(courseId); + const courseProgress = await this.getCourseCompletionStatus(courseId, studentId); const courseFinishedChapterCount = await this.getCourseFinishedChapterCount( courseId, studentId, ); - if (courseCompleted.courseCompleted) { + if (courseProgress.courseIsCompleted) { await this.updateStudentCourseStats( studentId, courseId, @@ -77,7 +118,7 @@ export class StudentLessonProgressService { return await this.statisticsRepository.updateCompletedAsFreemiumCoursesStats(courseId); } - if (courseCompleted.state !== PROGRESS_STATUSES.IN_PROGRESS) { + if (courseProgress.progress !== PROGRESS_STATUSES.IN_PROGRESS) { return await this.updateStudentCourseStats( studentId, courseId, @@ -104,71 +145,38 @@ export class StudentLessonProgressService { return finishedLessonsCount.count; } - // TODO: refactor this to use correct new names - private async getCourseCompletionStatus(courseId: UUIDType) { - const [courseCompleted] = await this.db.execute(sql` - WITH lesson_count AS ( - SELECT - course_lessons.lesson_id AS lesson_id, - COUNT(lesson_items.id) AS lesson_count - FROM - course_lessons - LEFT JOIN lesson_items ON course_lessons.lesson_id = lesson_items.lesson_id - WHERE - course_lessons.course_id = ${courseId} - AND lesson_items.lesson_item_type != ${LESSON_ITEM_TYPE.text_block.key} - GROUP BY - course_lessons.lesson_id - ), - completed_lesson_count AS ( - SELECT - course_lessons.lesson_id AS lesson_id, - COUNT(student_completed_lesson_items.id) AS completed_lesson_count - FROM - course_lessons - LEFT JOIN student_completed_lesson_items ON course_lessons.lesson_id = student_completed_lesson_items.lesson_id - WHERE - course_lessons.course_id = ${courseId} - GROUP BY - course_lessons.lesson_id - ), - lesson_completion_status AS ( - SELECT - lc.lesson_id, - CASE - WHEN lc.lesson_count = clc.completed_lesson_count THEN 1 - ELSE 0 - END AS is_lesson_completed - FROM - lesson_count lc - JOIN completed_lesson_count clc ON lc.lesson_id = clc.lesson_id - ), - course_completion_status AS ( - SELECT - CASE - WHEN COUNT(*) = SUM(is_lesson_completed) THEN true - ELSE false - END AS is_course_completed - FROM - lesson_completion_status - ) - SELECT is_course_completed:: BOOLEAN AS "courseCompleted", student_courses.state as "state" - FROM course_completion_status - LEFT JOIN student_courses ON student_courses.course_id = ${courseId} - `); - - return courseCompleted; + private async getCourseCompletionStatus(courseId: UUIDType, studentId: UUIDType) { + const [courseCompletedStatus] = await this.db + .select({ + courseIsCompleted: sql`${studentCourses.finishedChapterCount} = ${courses.chapterCount}`, + progress: sql`${studentCourses.progress}`, + }) + .from(studentCourses) + .leftJoin(courses, and(eq(courses.id, studentCourses.courseId))) + .where(and(eq(studentCourses.courseId, courseId), eq(studentCourses.studentId, studentId))); + + return { + courseIsCompleted: courseCompletedStatus.courseIsCompleted, + progress: courseCompletedStatus.progress, + }; } private async updateStudentCourseStats( studentId: UUIDType, courseId: UUIDType, - progress: string, + progress: ProgressStatus, finishedChapterCount: number, ) { + if (progress === PROGRESS_STATUSES.COMPLETED) { + return await this.db + .update(studentCourses) + .set({ progress, completedAt: sql`now()`, finishedChapterCount }) + .where(and(eq(studentCourses.studentId, studentId), eq(studentCourses.courseId, courseId))); + } + return await this.db .update(studentCourses) - .set({ progress, completedAt: sql`now()`, finishedChapterCount }) + .set({ progress, finishedChapterCount }) .where(and(eq(studentCourses.studentId, studentId), eq(studentCourses.courseId, courseId))); } } diff --git a/apps/web/app/api/generated-api.ts b/apps/web/app/api/generated-api.ts index 409f1d53..d1d2b570 100644 --- a/apps/web/app/api/generated-api.ts +++ b/apps/web/app/api/generated-api.ts @@ -609,6 +609,25 @@ export interface FileUploadResponse { fileUrl: string; } +export interface GetLessonByIdResponse { + data: { + /** @format uuid */ + id: string; + title: string; + type: string; + description: string; + fileType: string | null; + fileUrl: string | null; + quizDetails?: { + questions: any[]; + questionCount: number; + correctAnswerCount: number | null; + wrongAnswerCount: number | null; + score: number | null; + }; + }; +} + export type BetaCreateLessonBody = { updatedAt?: string; title: string; @@ -1960,6 +1979,20 @@ export class API extends HttpClient + this.request({ + path: `/api/lesson/${id}`, + method: "GET", + format: "json", + ...params, + }), + /** * No description *