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:
@@ -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);
|
||||
|
||||
if (!oldLessonLanguages || oldLessonLanguages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 (!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]
|
||||
};
|
||||
}
|
||||
|
||||
async function cloneLessonSections(sections: LessonSection[], courseId: string): Promise<CloneSectionsResponse> {
|
||||
if (!sections || sections.length === 0) {
|
||||
return { newSections: [], sectionIdMap: undefined };
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
return { newSections: newSections || [], sectionIdMap };
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
return { newLessons };
|
||||
}
|
||||
|
||||
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,
|
||||
// Map lesson languages to new lesson IDs
|
||||
const newLessonLanguages = oldLessonLanguages.map((lang) => ({
|
||||
content: lang.content,
|
||||
locale: lang.locale
|
||||
locale: lang.locale,
|
||||
lessonId: lessonIdMap.get(lang.lessonId!)!
|
||||
}));
|
||||
|
||||
const { error } = await supabase.from('lesson_language').insert(clonedLanguages).select();
|
||||
|
||||
if (error) {
|
||||
console.error('Error cloning lesson languages:', error);
|
||||
throw error;
|
||||
}
|
||||
// Bulk insert lesson languages
|
||||
await createLessonLanguages(newLessonLanguages);
|
||||
}
|
||||
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
|
||||
async function cloneExercises(lessons: Lesson[]): Promise<boolean> {
|
||||
// Clone exercises, questions, and options
|
||||
for (const lesson of lessons) {
|
||||
const { id: lessonId, exercise = [] } = lesson;
|
||||
// 3. Create lesson ID map
|
||||
const lessonIdMap = new Map<string, string>();
|
||||
oldLessons.forEach((oldLesson, index) => {
|
||||
lessonIdMap.set(oldLesson.id, newLessons[index].id);
|
||||
});
|
||||
|
||||
// Exercises
|
||||
for (const exerciseItem of exercise) {
|
||||
const { title, description, questions = [] } = exerciseItem;
|
||||
// 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!)!
|
||||
}))
|
||||
);
|
||||
|
||||
// Insert exercise
|
||||
const { data } = await supabase
|
||||
.from('exercise')
|
||||
.insert({
|
||||
description,
|
||||
due_by: new Date().toISOString(),
|
||||
lesson_id: lessonId,
|
||||
title
|
||||
})
|
||||
.select();
|
||||
// 5. Create exercise ID map
|
||||
const exerciseIdMap = new Map<string, string>();
|
||||
oldExercises.forEach((oldExercise, index) => {
|
||||
exerciseIdMap.set(oldExercise.id, newExercises[index].id);
|
||||
});
|
||||
|
||||
const newExercise = Array.isArray(data) ? data[0] : data;
|
||||
// 6. Fetch all questions for old exercises
|
||||
const oldExerciseIds = oldExercises.map((ex) => ex.id);
|
||||
const oldQuestions = await getQuestionsByExerciseIds(oldExerciseIds);
|
||||
|
||||
if (!newExercise) continue;
|
||||
if (!oldQuestions || oldQuestions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Questions
|
||||
for (const question of questions) {
|
||||
const { options = [] } = question;
|
||||
|
||||
// Insert question
|
||||
const { data: questionData } = await supabase
|
||||
.from('question')
|
||||
.insert({
|
||||
// 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,
|
||||
question_type_id: question.question_type_id,
|
||||
exercise_id: newExercise.id
|
||||
})
|
||||
.select();
|
||||
questionTypeId: question.questionTypeId,
|
||||
exerciseId: exerciseIdMap.get(question.exerciseId)!
|
||||
}))
|
||||
);
|
||||
|
||||
const newQuestion = Array.isArray(questionData) ? questionData[0] : questionData;
|
||||
// 8. Create question ID map
|
||||
const questionIdMap = new Map<number, number>();
|
||||
oldQuestions.forEach((oldQuestion, index) => {
|
||||
questionIdMap.set(oldQuestion.id, newQuestions[index].id);
|
||||
});
|
||||
|
||||
if (!newQuestion) continue;
|
||||
// 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);
|
||||
|
||||
// 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) => ({
|
||||
if (oldQuestionIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
is_correct: option.is_correct,
|
||||
question_id: newQuestion.id
|
||||
}));
|
||||
|
||||
// Insert options in batch
|
||||
await supabase.from('option').insert(newOptions).select();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
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
|
||||
});
|
||||
|
||||
if (!course) {
|
||||
throw new Error('Course not found');
|
||||
}
|
||||
// 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
|
||||
});
|
||||
|
||||
// 1. Clone course and group
|
||||
const { newCourse } = await cloneGroupAndBasicCourse(
|
||||
course,
|
||||
newTitle,
|
||||
userId,
|
||||
newDescription,
|
||||
newSlug,
|
||||
organizationId
|
||||
// 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
|
||||
}))
|
||||
);
|
||||
|
||||
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];
|
||||
}
|
||||
});
|
||||
// create map
|
||||
sectionMap = new Map(oldSections.map((old, i) => [old.id, newSections[i].id]));
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
// 6. clone lessons
|
||||
const oldLessons = await getLessonsByCourseId(courseId);
|
||||
|
||||
// 4. Clone lessons
|
||||
const { newLessons } = await cloneLessons(allLessons, newCourse.id, sectionIdMap);
|
||||
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
|
||||
}))
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -42,5 +42,6 @@
|
||||
"overrides": {
|
||||
"zod": "^4.1.8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8"
|
||||
}
|
||||
|
||||
@@ -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,6 +11,7 @@ import { db } from '@db/drizzle';
|
||||
* @returns Array of courses with lesson counts and organization info
|
||||
*/
|
||||
export const getCoursesBySiteName = async (siteName: string) => {
|
||||
try {
|
||||
const result = await db
|
||||
.select({
|
||||
course: schema.course,
|
||||
@@ -47,4 +49,23 @@ export const getCoursesBySiteName = async (siteName: string) => {
|
||||
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';
|
||||
@@ -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'}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './group';
|
||||
@@ -1,3 +1,6 @@
|
||||
export * from './organization';
|
||||
export * from './auth';
|
||||
export * from './course';
|
||||
export * from './group';
|
||||
export * from './lesson';
|
||||
export * from './exercise';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './lesson';
|
||||
@@ -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'}`);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user