Files
classroomio/apps/api/src/services/course/clone.ts
T

233 lines
6.6 KiB
TypeScript
Raw Normal View History

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';
2025-10-29 09:57:29 +01:00
// Constants
const QUESTION_TYPE_TEXTAREA = 2; // Paragraph question type
async function cloneLessonLanguages(newLessons: TLesson[], oldLessons: TLesson[]): Promise<void> {
// Extract old lesson IDs
const oldLessonIds = oldLessons.map((lesson) => lesson.id);
2025-10-29 09:57:29 +01:00
// Fetch all lesson languages for the old lessons
const oldLessonLanguages = await getLessonLanguagesByLessonIds(oldLessonIds);
2025-10-29 09:57:29 +01:00
if (!oldLessonLanguages || oldLessonLanguages.length === 0) {
return;
2025-10-29 09:57:29 +01:00
}
// 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);
2025-10-29 09:57:29 +01:00
});
// 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);
2025-10-29 09:57:29 +01:00
}
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;
2025-10-29 09:57:29 +01:00
}
// 3. Create lesson ID map
const lessonIdMap = new Map<string, string>();
oldLessons.forEach((oldLesson, index) => {
lessonIdMap.set(oldLesson.id, newLessons[index].id);
2025-10-29 09:57:29 +01:00
});
// 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!)!
}))
);
2025-10-29 09:57:29 +01:00
// 5. Create exercise ID map
const exerciseIdMap = new Map<string, string>();
oldExercises.forEach((oldExercise, index) => {
exerciseIdMap.set(oldExercise.id, newExercises[index].id);
2025-10-29 09:57:29 +01:00
});
// 6. Fetch all questions for old exercises
const oldExerciseIds = oldExercises.map((ex) => ex.id);
const oldQuestions = await getQuestionsByExerciseIds(oldExerciseIds);
2025-10-29 09:57:29 +01:00
if (!oldQuestions || oldQuestions.length === 0) {
return;
2025-10-29 09:57:29 +01:00
}
// 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)!
}))
);
2025-10-29 09:57:29 +01:00
// 8. Create question ID map
const questionIdMap = new Map<number, number>();
oldQuestions.forEach((oldQuestion, index) => {
questionIdMap.set(oldQuestion.id, newQuestions[index].id);
});
2025-10-29 09:57:29 +01:00
// 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);
2025-10-29 09:57:29 +01:00
if (oldQuestionIds.length === 0) {
return;
2025-10-29 09:57:29 +01:00
}
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)!
}))
);
2025-10-29 09:57:29 +01:00
}
export async function cloneCourse(
courseId: string,
newTitle: string,
userId: string,
newDescription?: string,
newSlug?: string,
organizationId?: string
): Promise<TCourse> {
// 1. fetch old course
const [course] = await getCourseById(courseId);
2025-10-29 09:57:29 +01:00
// 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]));
2025-10-29 09:57:29 +01:00
}
// 6. clone lessons
const oldLessons = await getLessonsByCourseId(courseId);
2025-10-29 09:57:29 +01:00
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
}))
2025-10-29 09:57:29 +01:00
);
// 7. clone languages, exercises, questions, options using bulk queries
await cloneLessonLanguages(newLessons, oldLessons);
await cloneExercises(newLessons, oldLessons);
2025-10-29 09:57:29 +01:00
return newCourse;
}