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

297 lines
8.6 KiB
TypeScript

import { TCourse, TCourseSection, TLesson } from '@db/types';
import {
addGroupMember,
createCourse,
createExercises,
createGroup,
createLessonLanguages,
createCourseSections,
createLessons,
createOptions,
createQuestions,
syncOptionIdSequence,
getCourseById,
getExercisesByLessonIds,
getLessonLanguagesByLessonIds,
getLessonsByCourseId,
getOptionsByQuestionIds,
getQuestionsByExerciseIds,
getCourseSectionsByCourseId,
getExerciseSectionsByExerciseIds,
createExerciseSections,
updateExerciseSection
} from '@db/queries';
import { ROLE } from '@cio/utils/constants';
import { QUESTION_TYPE_IDS } from '@cio/question-types';
async function cloneLessonLanguages(newLessons: TLesson[], oldLessons: TLesson[]): Promise<void> {
// Extract old lesson IDs
const oldLessonIds = oldLessons.map((lesson) => lesson.id);
// Fetch all lesson languages for the old lessons
const oldLessonLanguages = await getLessonLanguagesByLessonIds(oldLessonIds);
if (!oldLessonLanguages || oldLessonLanguages.length === 0) {
return;
}
// 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);
});
// 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 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;
}
// 3. Create lesson ID map
const lessonIdMap = new Map<string, string>();
oldLessons.forEach((oldLesson, index) => {
lessonIdMap.set(oldLesson.id, newLessons[index].id);
});
// 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!)!,
sectionDisplayMode: exercise.sectionDisplayMode
}))
);
// 5. Create exercise ID map
const exerciseIdMap = new Map<string, string>();
oldExercises.forEach((oldExercise, index) => {
exerciseIdMap.set(oldExercise.id, newExercises[index].id);
});
// 6. Fetch all questions for old exercises
const oldExerciseIds = oldExercises.map((ex) => ex.id);
const oldSections = await getExerciseSectionsByExerciseIds(oldExerciseIds);
const exerciseSectionIdMap = new Map<string, string>();
if (oldSections.length > 0) {
const newSections = await createExerciseSections(
oldSections.map((section) => ({
title: section.title,
description: section.description,
order: section.order,
colorTheme: section.colorTheme,
afterBehavior: section.afterBehavior,
exerciseId: exerciseIdMap.get(section.exerciseId)!
}))
);
oldSections.forEach((oldSection, index) => {
const newSection = newSections[index];
if (newSection) {
exerciseSectionIdMap.set(oldSection.id, newSection.id);
}
});
await Promise.all(
newSections.map((newSection) => {
const behavior = newSection.afterBehavior as { action?: string; exerciseSectionId?: string };
if (behavior.action !== 'go_to_section' || !behavior.exerciseSectionId) {
return null;
}
const remappedId = exerciseSectionIdMap.get(behavior.exerciseSectionId);
if (!remappedId) {
return null;
}
return updateExerciseSection(newSection.id, {
afterBehavior: { action: 'go_to_section', exerciseSectionId: remappedId }
});
})
);
}
const oldQuestions = await getQuestionsByExerciseIds(oldExerciseIds);
if (!oldQuestions || oldQuestions.length === 0) {
return;
}
// 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,
settings: question.settings,
exerciseSectionId: question.exerciseSectionId
? (exerciseSectionIdMap.get(question.exerciseSectionId) ?? null)
: null,
exerciseId: exerciseIdMap.get(question.exerciseId)!
}))
);
// 8. Create question ID map
const questionIdMap = new Map<number, number>();
oldQuestions.forEach((oldQuestion, index) => {
const newQuestionId = newQuestions[index]?.id;
const oldQuestionId = oldQuestion.id;
if (newQuestionId !== undefined && oldQuestionId !== undefined) {
questionIdMap.set(oldQuestionId, newQuestionId);
}
});
// 9. Fetch all options for old questions (except for paragraph type questions)
const oldQuestionIds = oldQuestions
.filter((q) => q.questionTypeId !== QUESTION_TYPE_IDS.TEXTAREA)
.map((q) => q.id)
.filter((id) => id !== undefined);
if (oldQuestionIds.length === 0) {
return;
}
const oldOptions = await getOptionsByQuestionIds(oldQuestionIds);
if (!oldOptions || oldOptions.length === 0) {
return;
}
// 10. Insert options with new question IDs
await syncOptionIdSequence();
await createOptions(
oldOptions
.map((option) => {
const newQuestionId = questionIdMap.get(option.questionId);
if (newQuestionId === undefined) {
return null;
}
return {
value: option.value,
label: option.label,
isCorrect: option.isCorrect,
settings: option.settings,
questionId: newQuestionId
};
})
.filter((opt) => opt !== null)
);
}
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);
// 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,
certificate: course.certificate,
status: course.status,
type: course.type
});
// 4. add group member
await addGroupMember({
profileId: userId,
email: '',
groupId: newGroup.id,
roleId: ROLE.TUTOR
});
// 5. clone sections
const oldSections = await getCourseSectionsByCourseId(courseId);
// Only create sections if there are any
let newSections: TCourseSection[] = [];
let sectionMap = new Map<string, string>();
if (oldSections.length > 0) {
newSections = await createCourseSections(
oldSections.map((section) => ({
title: section.title,
order: section.order,
courseId: newCourse.id
}))
);
// create map
sectionMap = new Map(oldSections.map((section, index) => [section.id, newSections[index].id]));
}
// 6. clone lessons
const oldLessons = await getLessonsByCourseId(courseId);
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
}))
);
// 7. clone languages, exercises, questions, options using bulk queries
await cloneLessonLanguages(newLessons, oldLessons);
await cloneExercises(newLessons, oldLessons);
return newCourse;
}