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
@@ -0,0 +1,10 @@
import { CERTIFICATE_THEME_IDS, DEFAULT_CERTIFICATE_THEME_ID, type CertificateThemeId } from './constants';
const CERTIFICATE_THEME_ID_SET = new Set<string>(CERTIFICATE_THEME_IDS);
export function parseCertificateThemeId(raw: string | null | undefined): CertificateThemeId {
if (raw != null && raw !== '' && CERTIFICATE_THEME_ID_SET.has(raw)) {
return raw as CertificateThemeId;
}
return DEFAULT_CERTIFICATE_THEME_ID;
}
@@ -37,6 +37,19 @@ export type CourseSortOrder = (typeof CourseSortOrder)[keyof typeof CourseSortOr
export const DEFAULT_SORT_ORDER: CourseSortOrder = CourseSortOrder.Desc;
export const CERTIFICATE_THEME_IDS = [
'professional',
'plain',
'purpleProfessionalBadge',
'blueProfessionalBadge',
'purpleBadgePattern',
'blueBadgePattern'
] as const;
export type CertificateThemeId = (typeof CERTIFICATE_THEME_IDS)[number];
export const DEFAULT_CERTIFICATE_THEME_ID: CertificateThemeId = 'professional';
const COURSE_SORT_ORDER_VALUES = new Set<string>(Object.values(CourseSortOrder));
export function parseCourseSortOrder(value: string | null | undefined): CourseSortOrder {
@@ -71,16 +71,17 @@ export function formatAnswers(data: {
answers: SubmissionAnswer[];
}): Record<string, AnswerData> {
const answers: Record<string, AnswerData> = {};
const questionByIdAndName: Record<string, string> = {};
const questionKeyById: Record<string, string> = {};
for (const question of data.questions) {
questionByIdAndName[String(question.id)] = question.name ?? '';
const key = question.name ? String(question.name) : String(question.id);
questionKeyById[String(question.id)] = key;
}
for (const answer of data.answers) {
const questionName = questionByIdAndName[String(answer.questionId)];
if (questionName && answer.answerData) {
answers[questionName] = answer.answerData;
const questionKey = questionKeyById[String(answer.questionId)];
if (questionKey && answer.answerData) {
answers[questionKey] = answer.answerData;
}
}
@@ -266,6 +266,13 @@ export type DeleteCourseRequest = (typeof classroomio.course)[':courseId']['$del
export type UpdateCourseContentRequest = (typeof classroomio.course)[':courseId']['content']['$put'];
export type DeleteCourseContentRequest = (typeof classroomio.course)[':courseId']['content']['$delete'];
export type GetCourseProgressRequest = (typeof classroomio.course)[':courseId']['progress']['$get'];
export type GetCertificationEvaluationRequest =
(typeof classroomio.course)[':courseId']['certification-evaluation']['$get'];
export type GetCertificationEvaluationSuccess = Extract<
InferResponseType<GetCertificationEvaluationRequest>,
{ success: true }
>;
export type CertificationEvaluationData = GetCertificationEvaluationSuccess['data'];
export type CloneCourseRequest = (typeof classroomio.course)[':courseId']['clone']['$post'];
export type EnrollCourseRequest = (typeof classroomio.course)[':courseId']['enroll']['$post'];
@@ -280,12 +287,17 @@ export type CourseMetadata = (NonNullable<ApiCourse['metadata']> extends object
: Record<string, unknown>) & {
isContentGroupingEnabled?: boolean;
};
export type CourseCertificate =
NonNullable<ApiCourse['certificate']> extends object
? NonNullable<ApiCourse['certificate']>
: Record<string, unknown>;
export type CourseContent = NonNullable<ApiCourse['content']>;
export type CourseContentSection = CourseContent['sections'][number];
export type CourseContentItem = CourseContent['items'][number];
export type Course = Omit<ApiCourse, 'metadata' | 'lessons' | 'sections' | 'exercises'> & {
metadata?: CourseMetadata | null;
certificate?: CourseCertificate | null;
content: CourseContent;
};