feat: remove supabase dependence during course cloning (#605)

* feat: Add .gitignore file and update package.json with pnpm version

feat: Enhance course queries with error handling and new course retrieval

feat: Implement exercise queries for creating and fetching exercises, questions, and options

feat: Create group queries for managing groups and group members

feat: Add lesson queries for creating lessons and fetching sections by course ID

feat: Update seed script to include seeding for users, profiles, accounts, organizations, groups, courses, lessons, exercises, questions, and options

* feat: Enhance course cloning functionality by refining course and lesson data handling

* chore: remove console.log

* feat: update role assignment in cloneCourse function and clean up unused types in database.ts
This commit is contained in:
Abdulmuizz Hamzat
2025-12-02 09:48:29 +01:00
committed by GitHub
parent 5cc8c47b72
commit 531516a8f7
13 changed files with 1130 additions and 488 deletions
+187 -316
View File
@@ -1,295 +1,136 @@
import { getSupabase } from '@api/utils/supabase';
import type { Course, Lesson, LessonSection, GroupMember } from '@api/types/database';
import type { PostgrestError } from '@supabase/supabase-js';
const supabase = getSupabase();
import { ROLE } from '@cio/utils/constants';
import {
addGroupMember,
createCourse,
createGroup,
createLessons,
createLessonSections,
getCourseById,
getLessonsByCourseId,
getSectionsByCourseId,
getLessonLanguagesByLessonIds,
createLessonLanguages,
getExercisesByLessonIds,
createExercises,
getQuestionsByExerciseIds,
createQuestions,
getOptionsByQuestionIds,
createOptions
} from '@db/queries';
import { TCourse, TLesson, TLessonSection } from '@db/types';
// Constants
const QUESTION_TYPE_TEXTAREA = 2; // Paragraph question type
const ROLE_TUTOR = 2; // Tutor role ID
// Reusable deep select query for lessons with all nested relations
const LESSON_DEEP_SELECT = `
*,
exercise!exercise_lesson_id_fkey(
id, title, description, due_by,
question!question_exercise_id_fkey(
*,
option!option_question_id_fkey(*),
question_type!question_question_type_id_fkey(id, label)
)
),
lesson_language!public_lesson_language_lesson_id_fkey(id, content, locale)
`;
async function cloneLessonLanguages(newLessons: TLesson[], oldLessons: TLesson[]): Promise<void> {
// Extract old lesson IDs
const oldLessonIds = oldLessons.map((lesson) => lesson.id);
interface FetchCourseDataResponse {
data: Course | null;
error: PostgrestError | null;
}
// Fetch all lesson languages for the old lessons
const oldLessonLanguages = await getLessonLanguagesByLessonIds(oldLessonIds);
interface CloneGroupResponse {
newCourse: Course;
}
interface CloneSectionsResponse {
newSections: LessonSection[];
sectionIdMap?: Map<string, string>;
}
interface CloneLessonsResponse {
newLessons: Lesson[] | null;
}
interface AddGroupMemberParams {
profile_id: string;
email: string;
group_id: string;
role_id: number;
}
// fetches course data with backward compatibility for courses that is either V1 or V2
async function fetchCourseData(courseId: string): Promise<FetchCourseDataResponse> {
const { data, error } = await supabase
.from('course')
.select(
`
*,
lesson_section(
id, title, order, created_at,
lessons:lesson(${LESSON_DEEP_SELECT})
),
lessons:lesson(${LESSON_DEEP_SELECT})
`
)
.eq('id', courseId)
.single();
return { data, error };
}
async function addGroupMember(params: AddGroupMemberParams): Promise<{
data: GroupMember[] | null;
error: PostgrestError | null;
}> {
const { data, error } = await supabase.from('groupmember').insert(params).select();
return { data, error };
}
async function cloneGroupAndBasicCourse(
course: Course,
newTitle: string,
userId: string,
newDescription?: string,
newSlug?: string,
organizationId?: string
): Promise<CloneGroupResponse> {
const { description } = course;
const finalDescription = newDescription || description;
const finalSlug = newSlug || null; // Use provided slug or null
// 1. Create group
const { data: newGroup, error: groupError } = await supabase
.from('group')
.insert({
name: newTitle,
description: finalDescription,
organization_id: organizationId
})
.select();
if (groupError) {
console.error('Group creation error:', groupError);
throw new Error(`Group creation failed: ${groupError.message}`);
if (!oldLessonLanguages || oldLessonLanguages.length === 0) {
return;
}
if (!newGroup || newGroup.length === 0) {
throw new Error('Group was not created successfully');
}
const { id: group_id } = newGroup[0];
// Remove nested relations and metadata that shouldn't be inserted
const { lessons, lesson_section, id, updated_at, created_at, ...courseData } = course as any;
// 2. Create course with group_id
const { data: newCourse, error: courseError } = await supabase
.from('course')
.insert({
...courseData,
title: newTitle,
description: finalDescription,
slug: finalSlug,
group_id
})
.select();
if (courseError) {
console.error('Course creation error:', courseError);
throw new Error(`Course creation failed: ${courseError.message}`);
}
if (!newCourse || newCourse.length === 0) {
throw new Error('Course was not created successfully');
}
// 3. Add group member (the user who is cloning)
await addGroupMember({
profile_id: userId,
email: '', // Email will be fetched from profile
group_id,
role_id: ROLE_TUTOR
// Create a map of old lesson ID to new lesson ID
const lessonIdMap = new Map<string, string>();
oldLessons.forEach((oldLesson, index) => {
lessonIdMap.set(oldLesson.id, newLessons[index].id);
});
return {
newCourse: newCourse[0]
};
// Map lesson languages to new lesson IDs
const newLessonLanguages = oldLessonLanguages.map((lang) => ({
content: lang.content,
locale: lang.locale,
lessonId: lessonIdMap.get(lang.lessonId!)!
}));
// Bulk insert lesson languages
await createLessonLanguages(newLessonLanguages);
}
async function cloneLessonSections(sections: LessonSection[], courseId: string): Promise<CloneSectionsResponse> {
if (!sections || sections.length === 0) {
return { newSections: [], sectionIdMap: undefined };
async function cloneExercises(newLessons: TLesson[], oldLessons: TLesson[]): Promise<void> {
// 1. Extract old lesson IDs
const oldLessonIds = oldLessons.map((lesson) => lesson.id);
// 2. Fetch all exercises for old lessons
const oldExercises = await getExercisesByLessonIds(oldLessonIds);
if (!oldExercises || oldExercises.length === 0) {
return;
}
const clonedSections = sections
.sort((a, b) => new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime())
.map((section) => ({
title: section.title,
order: section.order,
course_id: courseId
}));
const { data: newSections } = await supabase.from('lesson_section').insert(clonedSections).select();
// Map old section IDs to new section IDs
const sectionIdMap = new Map<string, string>();
sections.forEach((oldSection, index) => {
if (newSections && newSections[index]) {
sectionIdMap.set(oldSection.id, newSections[index].id);
}
// 3. Create lesson ID map
const lessonIdMap = new Map<string, string>();
oldLessons.forEach((oldLesson, index) => {
lessonIdMap.set(oldLesson.id, newLessons[index].id);
});
return { newSections: newSections || [], sectionIdMap };
}
// 4. Insert exercises with new lesson IDs
const newExercises = await createExercises(
oldExercises.map((exercise) => ({
title: exercise.title,
description: exercise.description,
dueBy: new Date().toISOString(),
lessonId: lessonIdMap.get(exercise.lessonId!)!
}))
);
async function cloneLessons(
lessons: Lesson[],
courseId: string,
sectionIdMap?: Map<string, string>
): Promise<CloneLessonsResponse> {
const clonedLessons = lessons
.sort((a, b) => new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime())
.map((lesson, index) => ({
call_url: lesson.call_url,
course_id: courseId,
section_id: lesson.section_id && sectionIdMap ? sectionIdMap.get(lesson.section_id) : null,
is_unlocked: lesson.is_unlocked,
lesson_at: new Date().toISOString(),
note: lesson.note,
order: lesson.order || index + 1,
public: lesson.public,
slide_url: lesson.slide_url,
title: lesson.title,
videos: lesson.videos
}));
const { data } = await supabase.from('lesson').insert(clonedLessons).select();
const newLessons = data?.map((dataItem, dataIndex) => {
return {
...dataItem,
exercise: lessons[dataIndex].exercise,
lesson_language: lessons[dataIndex].lesson_language
};
// 5. Create exercise ID map
const exerciseIdMap = new Map<string, string>();
oldExercises.forEach((oldExercise, index) => {
exerciseIdMap.set(oldExercise.id, newExercises[index].id);
});
return { newLessons };
}
// 6. Fetch all questions for old exercises
const oldExerciseIds = oldExercises.map((ex) => ex.id);
const oldQuestions = await getQuestionsByExerciseIds(oldExerciseIds);
async function cloneLessonLanguages(lessons: Lesson[]): Promise<boolean> {
for (const lesson of lessons) {
const { id: lessonId, lesson_language = [] } = lesson;
if (!lesson_language || lesson_language.length === 0) continue;
const clonedLanguages = lesson_language.map((lang: any) => ({
lesson_id: lessonId,
content: lang.content,
locale: lang.locale
}));
const { error } = await supabase.from('lesson_language').insert(clonedLanguages).select();
if (error) {
console.error('Error cloning lesson languages:', error);
throw error;
}
if (!oldQuestions || oldQuestions.length === 0) {
return;
}
return true;
}
// 7. Insert questions with new exercise IDs
const newQuestions = await createQuestions(
oldQuestions.map((question) => ({
name: question.name,
title: question.title,
points: question.points,
order: question.order,
questionTypeId: question.questionTypeId,
exerciseId: exerciseIdMap.get(question.exerciseId)!
}))
);
async function cloneExercises(lessons: Lesson[]): Promise<boolean> {
// Clone exercises, questions, and options
for (const lesson of lessons) {
const { id: lessonId, exercise = [] } = lesson;
// 8. Create question ID map
const questionIdMap = new Map<number, number>();
oldQuestions.forEach((oldQuestion, index) => {
questionIdMap.set(oldQuestion.id, newQuestions[index].id);
});
// Exercises
for (const exerciseItem of exercise) {
const { title, description, questions = [] } = exerciseItem;
// 9. Fetch all options for old questions (except for paragraph type questions)
const oldQuestionIds = oldQuestions.filter((q) => q.questionTypeId !== QUESTION_TYPE_TEXTAREA).map((q) => q.id);
// Insert exercise
const { data } = await supabase
.from('exercise')
.insert({
description,
due_by: new Date().toISOString(),
lesson_id: lessonId,
title
})
.select();
const newExercise = Array.isArray(data) ? data[0] : data;
if (!newExercise) continue;
// Questions
for (const question of questions) {
const { options = [] } = question;
// Insert question
const { data: questionData } = await supabase
.from('question')
.insert({
name: question.name,
title: question.title,
points: question.points,
order: question.order,
question_type_id: question.question_type_id,
exercise_id: newExercise.id
})
.select();
const newQuestion = Array.isArray(questionData) ? questionData[0] : questionData;
if (!newQuestion) continue;
// Options: Don't map options for 'Paragraph' questions
if (QUESTION_TYPE_TEXTAREA !== newQuestion.question_type_id && options.length > 0) {
const newOptions = options.map((option: any) => ({
value: option.value,
label: option.label,
is_correct: option.is_correct,
question_id: newQuestion.id
}));
// Insert options in batch
await supabase.from('option').insert(newOptions).select();
}
}
}
if (oldQuestionIds.length === 0) {
return;
}
return true;
const oldOptions = await getOptionsByQuestionIds(oldQuestionIds);
if (!oldOptions || oldOptions.length === 0) {
return;
}
// 10. Insert options with new question IDs
await createOptions(
oldOptions.map((option) => ({
value: option.value,
label: option.label,
isCorrect: option.isCorrect,
questionId: questionIdMap.get(option.questionId)!
}))
);
}
export async function cloneCourse(
@@ -299,63 +140,93 @@ export async function cloneCourse(
newDescription?: string,
newSlug?: string,
organizationId?: string
): Promise<Course> {
const { data: course, error } = await fetchCourseData(courseId);
): Promise<TCourse> {
// 1. fetch old course
const [course] = await getCourseById(courseId);
if (error) {
console.error('Error when cloning', error);
throw error;
// 2. create group
const [newGroup] = await createGroup({
name: newTitle,
description: newDescription ?? course.description,
organizationId: organizationId
});
// 3. create course for that group
// Explicitly select only the fields we want to copy (exclude id, createdAt, updatedAt)
const [newCourse] = await createCourse({
title: newTitle,
description: course.description,
overview: course.overview,
groupId: newGroup.id,
isTemplate: course.isTemplate,
logo: course.logo,
slug: newSlug ?? null,
metadata: course.metadata,
cost: course.cost,
currency: course.currency,
bannerImage: course.bannerImage,
isPublished: course.isPublished,
isCertificateDownloadable: course.isCertificateDownloadable,
certificateTheme: course.certificateTheme,
status: course.status,
type: course.type,
version: course.version
});
// 4. add group member
await addGroupMember({
profileId: userId,
email: '',
groupId: newGroup.id,
roleId: ROLE.TUTOR
});
// 5. clone sections
const oldSections = await getSectionsByCourseId(courseId);
// Only create sections if there are any
let newSections: TLessonSection[] = [];
let sectionMap = new Map<string, string>();
if (oldSections.length > 0) {
newSections = await createLessonSections(
oldSections.map((sec) => ({
title: sec.title,
order: sec.order,
courseId: newCourse.id
}))
);
// create map
sectionMap = new Map(oldSections.map((old, i) => [old.id, newSections[i].id]));
}
if (!course) {
throw new Error('Course not found');
}
// 6. clone lessons
const oldLessons = await getLessonsByCourseId(courseId);
// 1. Clone course and group
const { newCourse } = await cloneGroupAndBasicCourse(
course,
newTitle,
userId,
newDescription,
newSlug,
organizationId
const newLessons = await createLessons(
oldLessons.map((lesson) => ({
note: lesson.note,
videoUrl: lesson.videoUrl,
slideUrl: lesson.slideUrl,
courseId: newCourse.id,
title: lesson.title,
public: lesson.public,
lessonAt: lesson.lessonAt,
teacherId: lesson.teacherId,
isComplete: lesson.isComplete,
callUrl: lesson.callUrl,
order: lesson.order,
isUnlocked: lesson.isUnlocked,
videos: lesson.videos,
sectionId: lesson.sectionId ? sectionMap.get(lesson.sectionId) : null,
documents: lesson.documents
}))
);
let sectionIdMap: Map<string, string> | undefined = undefined;
let allLessons: Lesson[] = [];
// 2. Clone sections if this is a V2 course with sections
// @ts-ignore - lesson_section comes from the query
if (course.lesson_section && course.lesson_section.length > 0) {
// @ts-ignore
const { sectionIdMap: map } = await cloneLessonSections(course.lesson_section, newCourse.id);
sectionIdMap = map;
// Collect all lessons from sections
// @ts-ignore
course.lesson_section.forEach((section: any) => {
if (section.lessons && section.lessons.length > 0) {
allLessons = [...allLessons, ...section.lessons];
}
});
}
// 3. Also add lessons that are not in sections (V1 courses or orphaned lessons)
// @ts-ignore - lessons comes from the query
if (course.lessons && course.lessons.length > 0) {
// @ts-ignore
const orphanedLessons = course.lessons.filter((lesson: Lesson) => !lesson.section_id || !sectionIdMap);
allLessons = [...allLessons, ...orphanedLessons];
}
// 4. Clone lessons
const { newLessons } = await cloneLessons(allLessons, newCourse.id, sectionIdMap);
// 5. Clone lesson languages
await cloneLessonLanguages(newLessons || []);
// 6. Clone exercises, questions, and options
await cloneExercises(newLessons || []);
// 7. clone languages, exercises, questions, options using bulk queries
await cloneLessonLanguages(newLessons, oldLessons);
await cloneExercises(newLessons, oldLessons);
return newCourse;
}
+2 -135
View File
@@ -1,145 +1,12 @@
// Database types for course cloning
export interface Course {
id: string;
title: string;
description: string;
type: string;
version: 'V1' | 'V2';
overview?: string;
created_at: string;
updated_at: string;
group_id?: string;
is_template?: boolean;
organization_id?: string;
logo?: string;
slug?: string;
metadata?: Record<string, unknown>;
cost?: number;
currency?: string;
is_certificate_downloadable?: boolean;
certificate_theme?: string;
status?: string;
is_published?: boolean;
lesson_section?: LessonSection[];
lessons?: Lesson[];
}
export interface LessonSection {
id: string;
title: string;
order: number;
course_id: string;
created_at: string;
updated_at?: string;
lessons?: Lesson[];
}
export interface Lesson {
id: string;
title: string;
note?: string;
videos?: unknown[];
slide_url?: string;
course_id: string;
section_id?: string;
created_at: string;
updated_at?: string;
public?: boolean;
lesson_at?: string;
teacher_id?: string;
is_unlocked?: boolean;
call_url?: string;
order?: number;
exercise?: Exercise[];
lesson_language?: LessonLanguage[];
}
export interface LessonLanguage {
id: number;
content: string;
lesson_id: string;
locale: 'en' | 'hi' | 'fr' | 'pt' | 'de' | 'vi' | 'ru' | 'es' | 'pl' | 'da';
}
export interface Exercise {
id: string;
title: string;
description?: string;
lesson_id?: string;
created_at?: string;
updated_at?: string;
due_by?: string;
questions?: Question[];
}
export interface Question {
id: number;
question_type_id: number;
title: string;
name?: string;
points?: number;
order?: number;
created_at?: string;
updated_at?: string;
exercise_id: string;
question_type?: QuestionType;
options?: Option[];
}
export interface QuestionType {
id: number;
label: string;
typename?: string;
}
export interface Option {
id: number;
label: string;
is_correct: boolean;
question_id: number;
value?: string;
created_at?: string;
updated_at?: string;
}
export interface Group {
id: string;
name: string;
description?: string;
created_at?: string;
updated_at?: string;
organization_id?: string;
}
export interface GroupMember {
id: string;
group_id: string;
role_id: number;
profile_id?: string;
email?: string;
created_at?: string;
assigned_student_id?: string;
}
export interface Profile {
id: string;
fullname: string;
username: string;
avatar_url?: string;
email?: string;
created_at?: string;
updated_at?: string;
}
import { TCourse } from '@db/types';
// Response types
export interface CloneCourseResponse {
success: boolean;
course: Course;
course: TCourse;
}
export interface CloneCourseErrorResponse {
error: string;
details?: string;
}
+23
View File
@@ -0,0 +1,23 @@
.DS_Store
/node_modules/
node_modules
/src/node_modules/@sapper/
yarn-error.log
.svelte-kit/
.env
/dist/
/cert/
package-lock.json
.pnpm-store/*
# Local Netlify folder
.netlify
# Sentry Auth Token
.env.sentry-build-plugin
/_supascripts/
data.sql
changes.sql
.svelte-kit
+2 -1
View File
@@ -42,5 +42,6 @@
"overrides": {
"zod": "^4.1.8"
}
}
},
"packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8"
}
+56 -35
View File
@@ -3,6 +3,7 @@ import * as schema from '@db/schema';
import { and, eq, sql } from 'drizzle-orm';
import { db } from '@db/drizzle';
import { TNewCourse } from '@db/types';
/**
* Gets courses by organization siteName
@@ -10,41 +11,61 @@ import { db } from '@db/drizzle';
* @returns Array of courses with lesson counts and organization info
*/
export const getCoursesBySiteName = async (siteName: string) => {
const result = await db
.select({
course: schema.course,
organization: {
id: schema.organization.id,
name: schema.organization.name,
siteName: schema.organization.siteName,
avatarUrl: schema.organization.avatarUrl
},
lessonCount: sql<number>`COUNT(${schema.lesson.id})`.as('lesson_count')
})
.from(schema.course)
.innerJoin(schema.group, eq(schema.course.groupId, schema.group.id))
.innerJoin(schema.organization, eq(schema.group.organizationId, schema.organization.id))
.leftJoin(schema.lesson, eq(schema.course.id, schema.lesson.courseId))
.where(
and(
eq(schema.organization.siteName, siteName),
eq(schema.course.status, 'ACTIVE'),
eq(schema.course.isPublished, true)
try {
const result = await db
.select({
course: schema.course,
organization: {
id: schema.organization.id,
name: schema.organization.name,
siteName: schema.organization.siteName,
avatarUrl: schema.organization.avatarUrl
},
lessonCount: sql<number>`COUNT(${schema.lesson.id})`.as('lesson_count')
})
.from(schema.course)
.innerJoin(schema.group, eq(schema.course.groupId, schema.group.id))
.innerJoin(schema.organization, eq(schema.group.organizationId, schema.organization.id))
.leftJoin(schema.lesson, eq(schema.course.id, schema.lesson.courseId))
.where(
and(
eq(schema.organization.siteName, siteName),
eq(schema.course.status, 'ACTIVE'),
eq(schema.course.isPublished, true)
)
)
)
.groupBy(
schema.course.id,
schema.organization.id,
schema.organization.name,
schema.organization.siteName,
schema.organization.avatarUrl
);
.groupBy(
schema.course.id,
schema.organization.id,
schema.organization.name,
schema.organization.siteName,
schema.organization.avatarUrl
);
return result.map((row) => ({
...row.course,
lessons: [{ count: Number(row.lessonCount) }],
group: {
organization: row.organization
}
}));
return result.map((row) => ({
...row.course,
lessons: [{ count: Number(row.lessonCount) }],
group: {
organization: row.organization
}
}));
} catch (error) {
throw new Error(`Failed to get courses by site name "${siteName}": ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
export async function getCourseById(courseId: string) {
try {
return await db.select().from(schema.course).where(eq(schema.course.id, courseId)).limit(1);
} catch (error) {
throw new Error(`Failed to get course by ID "${courseId}": ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function createCourse(newCourse: TNewCourse) {
try {
return await db.insert(schema.course).values(newCourse).returning();
} catch (error) {
throw new Error(`Failed to create course: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
@@ -0,0 +1,62 @@
import { db, eq, inArray } from '@db/drizzle';
import { TNewExercise, TNewQuestion, TNewOption, TExercise } from '@db/types';
import * as schema from '@db/schema';
export async function createExercises(values: TNewExercise[]) {
try {
return db.insert(schema.exercise).values(values).returning();
} catch (error) {
throw new Error(`Failed to create exercises: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function getExercisesByLessonIds(lessonIds: string[]): Promise<TExercise[]> {
if (lessonIds.length === 0) return [];
try {
return db.select().from(schema.exercise).where(inArray(schema.exercise.lessonId, lessonIds));
} catch (error) {
throw new Error(
`Failed to get exercises by lesson IDs: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
export async function createQuestions(values: TNewQuestion[]) {
if (values.length === 0) return [];
try {
return db.insert(schema.question).values(values).returning();
} catch (error) {
throw new Error(`Failed to create questions: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function getQuestionsByExerciseIds(exerciseIds: string[]): Promise<TNewQuestion[]> {
if (exerciseIds.length === 0) return [];
try {
return db.select().from(schema.question).where(inArray(schema.question.exerciseId, exerciseIds));
} catch (error) {
throw new Error(
`Failed to get questions by exercise IDs: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
export async function createOptions(values: TNewOption[]) {
if (values.length === 0) return [];
try {
return db.insert(schema.option).values(values).returning();
} catch (error) {
throw new Error(`Failed to create options: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function getOptionsByQuestionIds(questionIds: number[]) {
if (questionIds.length === 0) return [];
try {
return db.select().from(schema.option).where(inArray(schema.option.questionId, questionIds));
} catch (error) {
throw new Error(
`Failed to get options by question IDs: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
@@ -0,0 +1 @@
export * from './exercise';
+19
View File
@@ -0,0 +1,19 @@
import { db } from '@db/drizzle';
import { TNewGroup, TNewGroupmember } from '@db/types';
import * as schema from '@db/schema';
export async function createGroup(values: TNewGroup) {
try {
return db.insert(schema.group).values(values).returning();
} catch (error) {
throw new Error(`Failed to create group: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function addGroupMember(values: TNewGroupmember) {
try {
return db.insert(schema.groupmember).values(values).returning();
} catch (error) {
throw new Error(`Failed to add group member: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
+1
View File
@@ -0,0 +1 @@
export * from './group';
+3
View File
@@ -1,3 +1,6 @@
export * from './organization';
export * from './auth';
export * from './course';
export * from './group';
export * from './lesson';
export * from './exercise';
+1
View File
@@ -0,0 +1 @@
export * from './lesson';
+59
View File
@@ -0,0 +1,59 @@
import { db, eq, inArray } from '@db/drizzle';
import * as schema from '@db/schema';
import { TNewLesson, TNewLessonSection, TNewLessonLanguage } from '@db/types';
export async function getSectionsByCourseId(courseId: string) {
try {
return db.select().from(schema.lessonSection).where(eq(schema.lessonSection.courseId, courseId));
} catch (error) {
throw new Error(
`Failed to get sections by course ID "${courseId}": ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
export async function createLessonSections(values: TNewLessonSection[]) {
try {
return db.insert(schema.lessonSection).values(values).returning();
} catch (error) {
throw new Error(`Failed to create lesson sections: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function getLessonsByCourseId(courseId: string) {
try {
return db.select().from(schema.lesson).where(eq(schema.lesson.courseId, courseId));
} catch (error) {
throw new Error(
`Failed to get lessons by course ID "${courseId}": ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
export async function createLessons(values: TNewLesson[]) {
try {
return db.insert(schema.lesson).values(values).returning();
} catch (error) {
throw new Error(`Failed to create lessons: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function getLessonLanguagesByLessonIds(lessonIds: string[]) {
if (lessonIds.length === 0) return [];
try {
return db.select().from(schema.lessonLanguage).where(inArray(schema.lessonLanguage.lessonId, lessonIds));
} catch (error) {
throw new Error(
`Failed to get lesson languages by lesson IDs: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
export async function createLessonLanguages(values: TNewLessonLanguage[]) {
if (values.length === 0) return [];
try {
return db.insert(schema.lessonLanguage).values(values).returning();
} catch (error) {
throw new Error(`Failed to create lesson languages: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
+714 -1
View File
@@ -1,6 +1,26 @@
import 'dotenv/config';
import { db, questionType, role, submissionstatus } from '../drizzle';
import {
account,
course,
db,
exercise,
group,
groupmember,
lesson,
option,
organization,
organizationmember,
profile,
question,
questionType,
role,
submissionstatus,
user
} from '../drizzle';
import { eq, or } from 'drizzle-orm';
import usersData from '../../users.json';
async function seed() {
try {
@@ -93,6 +113,699 @@ async function seed() {
console.log(' ✓ Question types already exist, skipping');
}
// Seed users (must be before profiles due to foreign key constraint)
console.log('📝 Seeding users...');
const existingUsers = await db.select().from(user);
const existingUserIds = existingUsers.map((u) => u.id);
const usersToInsert = usersData
.map((userData: any) => ({
id: userData.id,
name: userData.profile_fullname,
email: userData.email,
emailVerified: !!userData.email_confirmed_at,
image: null,
role: null,
banned: false,
isAnonymous: false
}))
.filter((u: any) => !existingUserIds.includes(u.id));
if (usersToInsert.length > 0) {
await db.insert(user).values(usersToInsert);
console.log(` ✓ Inserted ${usersToInsert.length} user(s)`);
} else {
console.log(' ✓ Users already exist, skipping');
}
// Seed profiles
console.log('📝 Seeding profiles...');
const existingProfiles = await db.select().from(profile);
const existingProfileIds = existingProfiles.map((p) => p.id);
const profilesToInsert = usersData
.map((user: any) => ({
id: user.id,
fullname: user.profile_fullname,
username: `${user.email.split('@')[0]}${Date.now()}`,
email: user.email,
avatarUrl: 'https://pgrest.classroomio.com/storage/v1/object/public/avatars/avatar.png',
canAddCourse: true,
isEmailVerified: true
}))
.filter((p: any) => !existingProfileIds.includes(p.id));
if (profilesToInsert.length > 0) {
await db.insert(profile).values(profilesToInsert);
console.log(` ✓ Inserted ${profilesToInsert.length} profile(s)`);
} else {
console.log(' ✓ Profiles already exist, skipping');
}
// Seed accounts (must be after users due to foreign key constraint)
console.log('📝 Seeding accounts...');
const existingAccounts = await db.select().from(account);
const existingAccountKeys = existingAccounts.map((a) => `${a.userId}-${a.providerId}`);
const accountsToInsert: any[] = [];
for (const userData of usersData) {
// Process identities to create accounts for all users in usersData
// (whether they were just inserted or already existed)
// Process identities to create accounts
if (userData.identities && Array.isArray(userData.identities)) {
for (const identity of userData.identities) {
const accountKey = `${userData.id}-${identity.provider === 'email' ? 'credential' : identity.provider}`;
if (existingAccountKeys.includes(accountKey)) {
continue;
}
if (identity.provider === 'email') {
// Create credential account for email provider
accountsToInsert.push({
userId: userData.id,
providerId: 'credential',
accountId: userData.id,
password: userData.encrypted_password
});
} else {
// For social providers, use the sub from identity_data
accountsToInsert.push({
userId: userData.id,
providerId: identity.provider,
accountId: identity.identity_data?.sub || userData.id
});
}
}
} else {
// Fallback: if no identities array, create credential account from email
const accountKey = `${userData.id}-credential`;
if (!existingAccountKeys.includes(accountKey)) {
accountsToInsert.push({
userId: userData.id,
providerId: 'credential',
accountId: userData.id,
password: userData.encrypted_password
});
}
}
}
if (accountsToInsert.length > 0) {
await db.insert(account).values(accountsToInsert);
console.log(` ✓ Inserted ${accountsToInsert.length} account(s)`);
} else {
console.log(' ✓ Accounts already exist, skipping');
}
// Seed organizations
console.log('📝 Seeding organizations...');
const existingOrgs = await db.select().from(organization);
const existingOrgIds = existingOrgs.map((o) => o.id);
const testOrgId = '1a1dcddd-1abc-4f72-b644-0bd18191a289';
const organizationsToInsert = [
{
id: testOrgId,
name: 'Udemy Test',
siteName: 'udemy-test',
settings: {},
landingpage: {},
theme: '',
customization: {
apps: { poll: true, comments: true },
course: { grading: true, newsfeed: true },
dashboard: { exercise: true, community: true, bannerText: '', bannerImage: '' }
}
}
].filter((o) => !existingOrgIds.includes(o.id));
if (organizationsToInsert.length > 0) {
await db.insert(organization).values(organizationsToInsert);
console.log(` ✓ Inserted ${organizationsToInsert.length} organization(s)`);
} else {
console.log(' ✓ Organizations already exist, skipping');
}
// Seed organization members
console.log('📝 Seeding organization members...');
const existingOrgMembers = await db.select().from(organizationmember);
const existingOrgMemberKeys = existingOrgMembers.map((om) => `${om.organizationId}-${om.profileId}`);
const adminUserId = '7ac00503-8519-43c8-a5ea-b79aeca900b1';
const studentUserId = '0c256e75-aa40-4f62-8d30-0217ca1c60d9';
const orgMembersToInsert = [
{
organizationId: testOrgId,
roleId: 1, // ADMIN
profileId: adminUserId,
verified: false
},
{
organizationId: testOrgId,
roleId: 3, // STUDENT
profileId: studentUserId,
verified: false
}
].filter((om) => !existingOrgMemberKeys.includes(`${om.organizationId}-${om.profileId}`));
if (orgMembersToInsert.length > 0) {
await db.insert(organizationmember).values(orgMembersToInsert);
console.log(` ✓ Inserted ${orgMembersToInsert.length} organization member(s)`);
} else {
console.log(' ✓ Organization members already exist, skipping');
}
// Seed groups
console.log('📝 Seeding groups...');
const existingGroups = await db.select().from(group);
const existingGroupIds = existingGroups.map((g) => g.id);
const mvcGroupId = 'c6b022fd-fff3-4f09-8960-c9cb06819761';
const reactGroupId = '04a250f1-bcb9-4e0d-a3d4-a01096e7a105';
const pandasGroupId = '0789ced2-b8f3-472c-97ff-bdde1e80dddf';
const groupsToInsert = [
{
id: mvcGroupId,
name: 'Getting started with MVC',
description:
"Embark on a comprehensive journey into the world of Model-View-Controller (MVC) architecture with our course, 'Getting Started with MVC.' Designed for beginners and aspiring developers, this course provides a solid foundation for understanding the principles and practices behind MVC, a widely adopted design pattern in software development.",
organizationId: testOrgId
},
{
id: reactGroupId,
name: 'Modern Web Development with React',
description:
"By the end of this course, you'll be equipped to build interactive and responsive web applications, making you a proficient React developer ready for the demands of today's web development landscape.",
organizationId: testOrgId
},
{
id: pandasGroupId,
name: 'Data Science with Python and Pandas',
description:
'Unlock the power of data with our "Data Science with Python and Pandas" course. Dive into Python programming fundamentals and then journey into the world of Pandas for efficient data manipulation and analysis.',
organizationId: testOrgId
}
].filter((g) => !existingGroupIds.includes(g.id));
if (groupsToInsert.length > 0) {
await db.insert(group).values(groupsToInsert);
console.log(` ✓ Inserted ${groupsToInsert.length} group(s)`);
} else {
console.log(' ✓ Groups already exist, skipping');
}
// Seed group members
console.log('📝 Seeding group members...');
const existingGroupMembers = await db.select().from(groupmember);
const existingGroupMemberKeys = existingGroupMembers.map((gm) => `${gm.groupId}-${gm.profileId || gm.email}`);
const groupMembersToInsert = [
{
groupId: mvcGroupId,
roleId: 2, // TUTOR
profileId: adminUserId,
email: 'admin@test.com'
},
{
groupId: reactGroupId,
roleId: 2, // TUTOR
profileId: adminUserId,
email: 'admin@test.com'
},
{
groupId: pandasGroupId,
roleId: 2, // TUTOR
profileId: adminUserId,
email: 'admin@test.com'
},
{
groupId: pandasGroupId,
roleId: 3, // STUDENT
profileId: studentUserId
}
].filter((gm) => !existingGroupMemberKeys.includes(`${gm.groupId}-${gm.profileId || gm.email}`));
if (groupMembersToInsert.length > 0) {
await db.insert(groupmember).values(groupMembersToInsert);
console.log(` ✓ Inserted ${groupMembersToInsert.length} group member(s)`);
} else {
console.log(' ✓ Group members already exist, skipping');
}
// Seed courses
console.log('📝 Seeding courses...');
const existingCourses = await db.select().from(course);
const existingCourseIds = existingCourses.map((c) => c.id);
const mvcCourseId = '98e6e798-f0bd-4f9d-a6f5-ce0816a4f97e';
const reactCourseId = '16e3bc8d-5d1b-4708-988e-93abae288ccf';
const pandasCourseId = 'f0a85d18-aff4-412f-b8e6-3b34ef098dce';
const coursesToInsert = [
{
id: mvcCourseId,
title: 'Getting started with MVC',
description:
"Embark on a comprehensive journey into the world of Model-View-Controller (MVC) architecture with our course, 'Getting Started with MVC.' Designed for beginners and aspiring developers, this course provides a solid foundation for understanding the principles and practices behind MVC, a widely adopted design pattern in software development.",
overview:
'<p>"Getting Started with MVC. is designed for beginners and aspiring developers, this course provides a solid foundation for understanding the principles and practices behind MVC, a widely adopted design pattern in software development.</p>',
groupId: mvcGroupId,
isTemplate: true,
logo: '',
slug: 'getting-started-with-mvc',
metadata: {
goals: '',
grading: false,
description: '',
requirements: '',
lessonDownload: false,
allowNewStudent: true,
lessonTabsOrder: [
{ id: 1, name: 'Note' },
{ id: 2, name: 'Slide' },
{ id: 3, name: 'Video' }
]
},
cost: 0,
currency: 'NGN',
isPublished: true,
isCertificateDownloadable: false,
status: 'ACTIVE'
},
{
id: reactCourseId,
title: 'Modern Web Development with React',
description:
"By the end of this course, you'll be equipped to build interactive and responsive web applications, making you a proficient React developer ready for the demands of today's web development landscape.",
overview: 'Welcome to this amazing course 🚀 ',
groupId: reactGroupId,
isTemplate: true,
logo: 'https://images.unsplash.com/photo-1565843708714-52ecf69ab81f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1MTE1NTV8MHwxfHNlYXJjaHwxOHx8cmVhY3QlMjBkZXZ8ZW58MHx8fHwxNzA3Nzk5NDMyfDA&ixlib=rb-4.0.3&q=80&w=1080',
slug: 'modern-web-development',
metadata: {
goals: '',
grading: true,
description: '',
requirements: '',
lessonDownload: false,
allowNewStudent: true,
lessonTabsOrder: [
{ id: 1, name: 'Note' },
{ id: 2, name: 'Slide' },
{ id: 3, name: 'Video' }
]
},
cost: 0,
currency: 'NGN',
isPublished: true,
isCertificateDownloadable: false,
status: 'ACTIVE'
},
{
id: pandasCourseId,
title: 'Data Science with Python and Pandas',
description:
'Unlock the power of data with our "Data Science with Python and Pandas" course. Dive into Python programming fundamentals and then journey into the world of Pandas for efficient data manipulation and analysis. Learn essential data cleaning and preprocessing techniques before venturing into statistical analysis using Pandas. Cap off your exploration with data visualization using Matplotlib and Seaborn.',
overview: 'Welcome to this amazing course 🚀 ',
groupId: pandasGroupId,
isTemplate: true,
logo: 'https://images.unsplash.com/photo-1518186285589-2f7649de83e0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1MTE1NTV8MHwxfHNlYXJjaHwxOHx8ZGF0YSUyMHNjaWVuY2V8ZW58MHx8fHwxNzA3Nzk5MzMwfDA&ixlib=rb-4.0.3&q=80&w=1080',
slug: 'data-science-with-python-and-pandas-1702919269375',
metadata: {
goals: '',
grading: true,
description: '',
requirements: '',
lessonDownload: true,
allowNewStudent: true,
lessonTabsOrder: [
{ id: 1, name: 'Note' },
{ id: 2, name: 'Slide' },
{ id: 3, name: 'Video' }
]
},
cost: 0,
currency: 'NGN',
isPublished: true,
isCertificateDownloadable: false,
status: 'ACTIVE'
}
].filter((c) => !existingCourseIds.includes(c.id));
if (coursesToInsert.length > 0) {
await db.insert(course).values(coursesToInsert);
console.log(` ✓ Inserted ${coursesToInsert.length} course(s)`);
} else {
console.log(' ✓ Courses already exist, skipping');
}
// Seed lessons
console.log('📝 Seeding lessons...');
const existingLessons = await db.select().from(lesson);
const existingLessonIds = existingLessons.map((l) => l.id);
const lessonsToInsert = [
// MVC Course lessons
{
id: '5c75f4f1-c222-44a9-a8c6-81773ea33872',
courseId: mvcCourseId,
title: 'Lesson 1: Introduction to MVC Architecture',
teacherId: adminUserId,
videos: [{ link: 'https://youtu.be/pXLWqkA87e4?si=rUHaBMnuFgAMjm2T', type: 'youtube', metadata: {} }],
isUnlocked: false
},
{
id: 'a99e65b7-1394-4751-ad8d-a5fb670ccb9e',
courseId: mvcCourseId,
title: 'Anatomy of MVC Components',
teacherId: adminUserId,
videos: [{ link: 'https://youtu.be/4Qfk8MhtZJU?si=VZ7cF-pjvm_RmFMp', type: 'youtube', metadata: {} }],
isUnlocked: false
},
{
id: '266b3daa-1eb2-401e-9510-1819952b44b7',
courseId: mvcCourseId,
title: 'Building Your First MVC Application',
teacherId: adminUserId,
videos: [{ link: 'https://www.youtube.com/watch?v=EMwu8F0dCXE', type: 'youtube', metadata: {} }],
isUnlocked: false
},
// React Course lessons
{
id: '6f2d8142-0903-425c-8534-f5105b624752',
courseId: reactCourseId,
title: 'Introduction to React: Understanding the Basics',
teacherId: adminUserId,
videos: [{ link: 'https://www.youtube.com/watch?v=H-PkPKF2Tfk', type: 'youtube', metadata: {} }],
isUnlocked: false
},
{
id: '0a39ab2f-9451-4a90-902c-3030bf965637',
courseId: reactCourseId,
title: 'Components and Props: Building Reusable UI Elements',
teacherId: adminUserId,
videos: [{ link: 'https://www.youtube.com/watch?v=H-PkPKF2Tfk', type: 'youtube', metadata: {} }],
isUnlocked: false
},
{
id: '80b79665-733b-41bf-9853-34fd8ab50496',
courseId: reactCourseId,
title: 'State and Lifecycle: Managing Data in React Applications',
teacherId: adminUserId,
videos: [{ link: 'https://www.youtube.com/watch?v=DveeFlWzWzc', type: 'youtube', metadata: {} }],
isUnlocked: false
},
// Pandas Course lessons
{
id: '5e5c8221-4c11-4c40-8664-11743bb79579',
courseId: pandasCourseId,
title: 'Python Essentials: An Introduction to Data Science',
teacherId: adminUserId,
videos: [{ link: 'https://www.youtube.com/watch?v=T5pRlIbr6gg&vl=en', type: 'youtube', metadata: {} }],
isUnlocked: true
},
{
id: '829da386-8ccd-4c81-b2fb-b9891102c83c',
courseId: pandasCourseId,
title: 'Delving into Data Analysis with Pandas',
teacherId: adminUserId,
videos: [{ link: 'https://www.youtube.com/watch?v=T5pRlIbr6gg&vl=en', type: 'youtube', metadata: {} }],
isUnlocked: true
},
{
id: '05f03084-3ff1-49e3-aa2a-7a13840cc4b1',
courseId: pandasCourseId,
title: 'Data Cleaning and Preprocessing Techniques',
teacherId: adminUserId,
videos: [{ link: 'https://www.youtube.com/watch?v=LI7s_lyooO8', type: 'youtube', metadata: {} }],
isUnlocked: true
}
].filter((l) => !existingLessonIds.includes(l.id));
if (lessonsToInsert.length > 0) {
await db.insert(lesson).values(lessonsToInsert);
console.log(` ✓ Inserted ${lessonsToInsert.length} lesson(s)`);
} else {
console.log(' ✓ Lessons already exist, skipping');
}
// Seed exercises
console.log('📝 Seeding exercises...');
const existingExercises = await db.select().from(exercise);
const existingExerciseIds = existingExercises.map((e) => e.id);
const exercisesToInsert = [
{
id: 'e2ea9fb8-6448-4f6c-a1d5-02c2b12cf862',
title: 'MVC essentials quiz',
description: "<p>This exercise tests your knowledge of what we've covered in the first lesson.</p>",
lessonId: '5c75f4f1-c222-44a9-a8c6-81773ea33872'
},
{
id: '8b6084b0-a936-411d-9408-a1289aeed068',
title: 'MVC Components quiz',
lessonId: 'a99e65b7-1394-4751-ad8d-a5fb670ccb9e'
},
{
id: '43f43374-7df0-4e02-b839-56403f71473a',
title: 'NodeJS MVC Quiz',
lessonId: '266b3daa-1eb2-401e-9510-1819952b44b7'
},
{
id: 'e78bfd24-8ac3-43e9-a117-a2f9d00f74b1',
title: 'Introduction To React: Quiz',
lessonId: '6f2d8142-0903-425c-8534-f5105b624752'
},
{
id: '43ead5d7-af88-47cf-8f86-99124f5eb0cd',
title: 'Components And Props Quiz',
lessonId: '0a39ab2f-9451-4a90-902c-3030bf965637'
},
{
id: 'b0770deb-a8a0-4efe-9d28-8bf1298c04b2',
title: 'React State And Lifecycle Quiz',
lessonId: '80b79665-733b-41bf-9853-34fd8ab50496'
},
{
id: '6f1063ed-3791-43fe-81e9-ad3b007834fa',
title: 'Introduction to Python - Quiz',
lessonId: '5e5c8221-4c11-4c40-8664-11743bb79579',
dueBy: '2023-12-21T18:21:00.000Z'
},
{
id: 'bd6e81c7-3d28-4037-acf0-a3028c583771',
title: 'Delving Into Data Analysis With Pandas - Quiz',
lessonId: '829da386-8ccd-4c81-b2fb-b9891102c83c'
},
{
id: 'd8cd1cf7-1951-46b3-ad1c-41e415185bc1',
title: 'Data Cleaning And Preprocessing Techniques - Quiz',
lessonId: '05f03084-3ff1-49e3-aa2a-7a13840cc4b1'
}
].filter((e) => !existingExerciseIds.includes(e.id));
if (exercisesToInsert.length > 0) {
await db.insert(exercise).values(exercisesToInsert);
console.log(` ✓ Inserted ${exercisesToInsert.length} exercise(s)`);
} else {
console.log(' ✓ Exercises already exist, skipping');
}
// Seed questions
console.log('📝 Seeding questions...');
const targetExerciseIds = [
'e2ea9fb8-6448-4f6c-a1d5-02c2b12cf862',
'e78bfd24-8ac3-43e9-a117-a2f9d00f74b1',
'bd6e81c7-3d28-4037-acf0-a3028c583771',
'd8cd1cf7-1951-46b3-ad1c-41e415185bc1'
];
const existingQuestions = await db
.select()
.from(question)
.where(
or(
eq(question.exerciseId, 'e2ea9fb8-6448-4f6c-a1d5-02c2b12cf862'),
eq(question.exerciseId, 'e78bfd24-8ac3-43e9-a117-a2f9d00f74b1'),
eq(question.exerciseId, 'bd6e81c7-3d28-4037-acf0-a3028c583771'),
eq(question.exerciseId, 'd8cd1cf7-1951-46b3-ad1c-41e415185bc1')
)
);
const existingQuestionExerciseIds = existingQuestions.map((q) => q.exerciseId);
const questionsToInsert = [
{
questionTypeId: 2, // CHECKBOX
title: 'What does MVC stand for',
exerciseId: 'e2ea9fb8-6448-4f6c-a1d5-02c2b12cf862',
points: 0,
order: 0
},
{
questionTypeId: 2, // CHECKBOX
title:
"What is the purpose of the virtual DOM in React, and how does it contribute to the framework's performance optimization?",
exerciseId: 'e78bfd24-8ac3-43e9-a117-a2f9d00f74b1',
points: 10,
order: 0
},
{
questionTypeId: 2, // CHECKBOX
title: 'When working with Pandas, what function is used to drop missing values from a DataFrame?',
exerciseId: 'bd6e81c7-3d28-4037-acf0-a3028c583771',
points: 20,
order: 0
},
{
questionTypeId: 2, // CHECKBOX
title: 'What is the primary purpose of data cleaning in the context of data preprocessing?',
exerciseId: 'd8cd1cf7-1951-46b3-ad1c-41e415185bc1',
points: 0,
order: 0
}
].filter((q) => !existingQuestionExerciseIds.includes(q.exerciseId));
let insertedQuestions: any[] = [];
if (questionsToInsert.length > 0) {
const result = await db.insert(question).values(questionsToInsert).returning();
insertedQuestions = result;
console.log(` ✓ Inserted ${questionsToInsert.length} question(s)`);
} else {
console.log(' ✓ Questions already exist, skipping');
insertedQuestions = existingQuestions;
}
// Seed options for questions
console.log('📝 Seeding question options...');
const questionIds = insertedQuestions.map((q) => q.id);
// Check if options already exist for these questions
let existingOptions: any[] = [];
if (questionIds.length > 0) {
const conditions = questionIds.map((id) => eq(option.questionId, id));
existingOptions = await db
.select()
.from(option)
.where(or(...conditions));
}
const existingOptionQuestionIds = new Set(existingOptions.map((o) => o.questionId));
// Get question IDs from inserted or existing questions
const mvcQuestion = insertedQuestions.find((q) => q.exerciseId === 'e2ea9fb8-6448-4f6c-a1d5-02c2b12cf862');
const reactQuestion = insertedQuestions.find((q) => q.exerciseId === 'e78bfd24-8ac3-43e9-a117-a2f9d00f74b1');
const pandasQuestion1 = insertedQuestions.find((q) => q.exerciseId === 'bd6e81c7-3d28-4037-acf0-a3028c583771');
const pandasQuestion2 = insertedQuestions.find((q) => q.exerciseId === 'd8cd1cf7-1951-46b3-ad1c-41e415185bc1');
const optionsToInsert = [
// MVC question options
...(mvcQuestion
? [
{
label: 'Modulus, View, Calculus',
isCorrect: false,
questionId: mvcQuestion.id
},
{
label: 'Mode, Vue, Controller',
isCorrect: false,
questionId: mvcQuestion.id
},
{
label: 'Model, View, Controller',
isCorrect: true,
questionId: mvcQuestion.id
}
]
: []),
// React question options
...(reactQuestion
? [
{
label: 'It stores React components in a centralized virtual memory.',
isCorrect: false,
questionId: reactQuestion.id
},
{
label: 'It facilitates direct communication between the browser and the server.',
isCorrect: false,
questionId: reactQuestion.id
},
{
label:
'It is an in-memory representation of the actual DOM, allowing React to efficiently update and render UI changes.',
isCorrect: true,
questionId: reactQuestion.id
},
{
label: 'It handles encryption and decryption for secure data transmission.',
isCorrect: false,
questionId: reactQuestion.id
}
]
: []),
// Pandas question 1 options
...(pandasQuestion1
? [
{
label: 'drop_null()',
isCorrect: false,
questionId: pandasQuestion1.id
},
{
label: 'remove_missing()',
isCorrect: false,
questionId: pandasQuestion1.id
},
{
label: 'dropna()',
isCorrect: true,
questionId: pandasQuestion1.id
},
{
label: 'clean_data()',
isCorrect: false,
questionId: pandasQuestion1.id
}
]
: []),
// Pandas question 2 options
...(pandasQuestion2
? [
{
label: 'To increase the size of the dataset',
isCorrect: false,
questionId: pandasQuestion2.id
},
{
label: 'To introduce noise and variability',
isCorrect: false,
questionId: pandasQuestion2.id
},
{
label: 'To remove errors and inconsistencies',
isCorrect: true,
questionId: pandasQuestion2.id
},
{
label: 'To speed up the data processing time',
isCorrect: false,
questionId: pandasQuestion2.id
}
]
: [])
].filter((o) => !existingOptionQuestionIds.has(o.questionId));
if (optionsToInsert.length > 0) {
await db.insert(option).values(optionsToInsert);
console.log(` ✓ Inserted ${optionsToInsert.length} option(s)`);
} else {
console.log(' ✓ Options already exist, skipping');
}
console.log('✅ Seed completed successfully!');
process.exit(0);
} catch (error) {