feat: certification rules, content sanitization, handle draft course updates and reset content, better redis logging

This commit is contained in:
Rotimi Best
2026-03-30 08:24:55 +01:00
parent ff2ad59377
commit 5a8f50f67b
131 changed files with 5643 additions and 2134 deletions
@@ -2,9 +2,20 @@ import { AppError, ErrorCodes } from '@api/utils/errors';
import { env } from '@api/config/env';
import { getDashboardBaseUrl } from '@api/config/dashboard-url';
import { createCourse, updateCourse } from '@api/services/course/course';
import { createExercise, getExercise, listExercises, replaceExerciseService } from '@api/services/exercise';
import { createCourseSection, listCourseSections, updateCourseSectionService } from '@api/services/course/section';
import { createLesson, listLessons, updateLessonService } from '@api/services/lesson';
import {
createExercise,
deleteExerciseService,
getExercise,
listExercises,
replaceExerciseService
} from '@api/services/exercise';
import {
createCourseSection,
deleteCourseSectionService,
listCourseSections,
updateCourseSectionService
} from '@api/services/course/section';
import { createLesson, listLessons, updateLessonService, deleteLessonService } from '@api/services/lesson';
import { upsertLessonLanguageService, updateLessonLanguageService } from '@api/services/lesson-language';
import {
createCourseImportDraft,
@@ -203,6 +214,43 @@ function parseStoredDraft(record: TCourseImportDraft): TCourseImportDraftPayload
return ZCourseImportDraftPayload.parse(record.draft);
}
async function reserveDraftPublishedCourseId(
orgId: string,
draftId: string,
draft: TCourseImportDraftPayload,
existingSummary: TCourseImportDraft['summary'],
courseId: string
) {
await updateCourseImportDraft(orgId, draftId, {
publishedCourseId: courseId,
summary: {
...(existingSummary ?? {}),
...buildDraftSummary(draft),
publishedCourseId: courseId
}
});
}
async function resetCourseContentForDraftRetry(courseId: string) {
const [exercises, lessons, sections] = await Promise.all([
listExercises(courseId),
listLessons(courseId),
listCourseSections(courseId)
]);
for (const exercise of exercises) {
await deleteExerciseService(exercise.id);
}
for (const lesson of lessons) {
await deleteLessonService(lesson.id);
}
for (const section of sections) {
await deleteCourseSectionService(section.id);
}
}
async function assertCourseBelongsToOrganization(orgId: string, courseId: string) {
const courseOrgId = await getCourseOrganizationId(courseId);
@@ -665,6 +713,14 @@ export async function publishCourseImportDraftService(
const courseDescription = overrides.description ?? draft.course.description;
const courseType = overrides.type ?? draft.course.type;
const courseMetadata = overrides.metadata ?? draft.course.metadata;
const courseUpdateFields: Partial<TCourse> = {
title: courseTitle,
description: courseDescription,
type: courseType
};
if (courseMetadata) {
courseUpdateFields.metadata = courseMetadata;
}
const sectionExternalIds = new Set(draft.sections.map((section) => section.externalId));
draft.lessons.forEach((lesson) => {
@@ -673,17 +729,31 @@ export async function publishCourseImportDraftService(
}
});
const courseResult = await createCourse(profileId, {
title: courseTitle,
description: courseDescription,
type: courseType,
organizationId: orgId
});
let courseId = existing.publishedCourseId;
const courseId = courseResult.course.id;
if (courseId) {
const [existingCourse] = await getCourseById(courseId);
if (!existingCourse || existingCourse.status === 'DELETED') {
courseId = null;
} else {
await updateCourse(courseId, courseUpdateFields);
await resetCourseContentForDraftRetry(courseId);
}
}
if (courseMetadata) {
await updateCourse(courseId, { metadata: courseMetadata });
if (!courseId) {
const courseResult = await createCourse(profileId, {
title: courseTitle,
description: courseDescription,
type: courseType,
organizationId: orgId
});
courseId = courseResult.course.id;
await reserveDraftPublishedCourseId(orgId, draftId, draft, existing.summary, courseId);
if (courseMetadata) {
await updateCourse(courseId, { metadata: courseMetadata });
}
}
const bannerImageUrl = await resolvePublishBannerImage(courseTitle, overrides);