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
@@ -1,7 +1,7 @@
import * as schema from '@db/schema';
import type { TNewQuestionAnswer, TNewSubmission, TQuestionAnswer, TSubmission } from '@db/types';
import { and, db, eq, inArray } from '@db/drizzle';
import { and, count, db, eq, inArray, sql } from '@db/drizzle';
/**
* Gets a submission by ID
@@ -24,6 +24,23 @@ export async function getSubmissionById(submissionId: string): Promise<TSubmissi
}
}
export async function hasSubmission(exerciseId: string, submittedBy: string): Promise<boolean> {
try {
const [result] = await db
.select({ total: count() })
.from(schema.submission)
.where(and(eq(schema.submission.exerciseId, exerciseId), eq(schema.submission.submittedBy, submittedBy)))
.limit(1);
return (result?.total ?? 0) > 0;
} catch (error) {
console.error('hasSubmission error:', error);
throw new Error(
`Failed to check submission existence: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Gets submissions by course ID with full related data (answers, profile, etc.)
* @param courseId Course ID
@@ -266,6 +283,21 @@ export async function upsertQuestionAnswer(data: TNewQuestionAnswer): Promise<TQ
}
}
export async function insertQuestionAnswersBatch(rows: TNewQuestionAnswer[]): Promise<TQuestionAnswer[]> {
if (rows.length === 0) {
return [];
}
try {
return db.insert(schema.questionAnswer).values(rows).returning();
} catch (error) {
console.error('insertQuestionAnswersBatch error:', error);
throw new Error(
`Failed to insert question answers batch: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Updates a question answer
* @param submissionId Submission ID
@@ -311,13 +343,15 @@ export async function updateSubmissionGrades(
): Promise<TSubmission | null> {
try {
return await db.transaction(async (tx) => {
for (const { questionId, points } of data.answers) {
await tx
.update(schema.questionAnswer)
.set({ point: points })
.where(
and(eq(schema.questionAnswer.submissionId, submissionId), eq(schema.questionAnswer.questionId, questionId))
);
if (data.answers.length > 0) {
const valueRows = data.answers.map((a) => sql`(${a.questionId}::bigint, ${a.points}::bigint)`);
await tx.execute(sql`
UPDATE question_answer AS qa
SET point = v.point
FROM (VALUES ${sql.join(valueRows, sql`, `)}) AS v(question_id, point)
WHERE qa.submission_id = ${submissionId}
AND qa.question_id = v.question_id
`);
}
const submissionData: Partial<TSubmission> = {