refactor(backend): Functions

This commit is contained in:
Tibo De Peuter 2025-03-22 18:38:38 +01:00
parent 5ec62554e3
commit 65c1a5e6b6
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
57 changed files with 172 additions and 117 deletions

View file

@ -26,7 +26,7 @@ app.use('/api', apiRouter);
// Swagger // Swagger
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
async function startServer() { async function startServer(): Promise<void> {
await initORM(); await initORM();
app.listen(port, () => { app.listen(port, () => {

View file

@ -47,7 +47,7 @@ export async function getStudentHandler(req: Request, res: Response): Promise<vo
res.status(201).json(user); res.status(201).json(user);
} }
export async function createStudentHandler(req: Request, res: Response) { export async function createStudentHandler(req: Request, res: Response): Promise<void> {
const userData = req.body as StudentDTO; const userData = req.body as StudentDTO;
if (!userData.username || !userData.firstName || !userData.lastName) { if (!userData.username || !userData.firstName || !userData.lastName) {
@ -61,7 +61,7 @@ export async function createStudentHandler(req: Request, res: Response) {
res.status(201).json(newUser); res.status(201).json(newUser);
} }
export async function deleteStudentHandler(req: Request, res: Response) { export async function deleteStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username; const username = req.params.username;
if (!username) { if (!username) {

View file

@ -30,7 +30,7 @@ export async function getSubmissionHandler(req: Request<SubmissionParams>, res:
res.json(submission); res.json(submission);
} }
export async function createSubmissionHandler(req: Request, res: Response) { export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
const submissionDTO = req.body as SubmissionDTO; const submissionDTO = req.body as SubmissionDTO;
const submission = await createSubmission(submissionDTO); const submission = await createSubmission(submissionDTO);
@ -42,7 +42,7 @@ export async function createSubmissionHandler(req: Request, res: Response) {
} }
} }
export async function deleteSubmissionHandler(req: Request, res: Response) { export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid; const hruid = req.params.hruid;
const submissionNumber = +req.params.id; const submissionNumber = +req.params.id;

View file

@ -50,7 +50,7 @@ export async function getTeacherHandler(req: Request, res: Response): Promise<vo
res.status(201).json(user); res.status(201).json(user);
} }
export async function createTeacherHandler(req: Request, res: Response) { export async function createTeacherHandler(req: Request, res: Response): Promise<void> {
const userData = req.body as TeacherDTO; const userData = req.body as TeacherDTO;
if (!userData.username || !userData.firstName || !userData.lastName) { if (!userData.username || !userData.firstName || !userData.lastName) {
@ -64,7 +64,7 @@ export async function createTeacherHandler(req: Request, res: Response) {
res.status(201).json(newUser); res.status(201).json(newUser);
} }
export async function deleteTeacherHandler(req: Request, res: Response) { export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username; const username = req.params.username;
if (!username) { if (!username) {

View file

@ -8,7 +8,7 @@ interface Translations {
}; };
} }
export function getThemes(req: Request, res: Response) { export function getThemes(req: Request, res: Response): void {
const language = (req.query.language as string)?.toLowerCase() || 'nl'; const language = (req.query.language as string)?.toLowerCase() || 'nl';
const translations = loadTranslations<Translations>(language); const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => ({ const themeList = themes.map((theme) => ({
@ -21,7 +21,7 @@ export function getThemes(req: Request, res: Response) {
res.json(themeList); res.json(themeList);
} }
export function getThemeByTitle(req: Request, res: Response) { export function getThemeByTitle(req: Request, res: Response): void {
const themeKey = req.params.theme; const themeKey = req.params.theme;
const theme = themes.find((t) => t.title === themeKey); const theme = themes.find((t) => t.title === themeKey);

View file

@ -47,7 +47,7 @@ export async function getUserHandler<T extends User>(req: Request, res: Response
} }
} }
export async function createUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>, userClass: new () => T) { export async function createUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>, userClass: new () => T): Promise<void> {
try { try {
getLogger().debug({ req: req }); getLogger().debug({ req: req });
const userData = req.body as UserDTO; const userData = req.body as UserDTO;
@ -67,7 +67,7 @@ export async function createUserHandler<T extends User>(req: Request, res: Respo
} }
} }
export async function deleteUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>) { export async function deleteUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
try { try {
const username = req.params.username; const username = req.params.username;

View file

@ -3,13 +3,13 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Class } from '../../entities/classes/class.entity.js'; import { Class } from '../../entities/classes/class.entity.js';
export class AssignmentRepository extends DwengoEntityRepository<Assignment> { export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
public findByClassAndId(within: Class, id: number): Promise<Assignment | null> { public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
return this.findOne({ within: within, id: id }); return this.findOne({ within: within, id: id });
} }
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
return this.findAll({ where: { within: within } }); return this.findAll({ where: { within: within } });
} }
public deleteByClassAndId(within: Class, id: number): Promise<void> { public async deleteByClassAndId(within: Class, id: number): Promise<void> {
return this.deleteWhere({ within: within, id: id }); return this.deleteWhere({ within: within, id: id });
} }
} }

View file

@ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
export class GroupRepository extends DwengoEntityRepository<Group> { export class GroupRepository extends DwengoEntityRepository<Group> {
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
return this.findOne( return this.findOne(
{ {
assignment: assignment, assignment: assignment,
@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> {
{ populate: ['members'] } { populate: ['members'] }
); );
} }
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
return this.findAll({ return this.findAll({
where: { assignment: assignment }, where: { assignment: assignment },
populate: ['members'], populate: ['members'],
}); });
} }
public findAllGroupsWithStudent(student: Student): Promise<Group[]> { public async findAllGroupsWithStudent(student: Student): Promise<Group[]> {
return this.find({ members: student }, { populate: ['members'] }); return this.find({ members: student }, { populate: ['members'] });
} }
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
assignment: assignment, assignment: assignment,
groupNumber: groupNumber, groupNumber: groupNumber,

View file

@ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
export class SubmissionRepository extends DwengoEntityRepository<Submission> { export class SubmissionRepository extends DwengoEntityRepository<Submission> {
public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> { public async findSubmissionByLearningObjectAndSubmissionNumber(
loId: LearningObjectIdentifier,
submissionNumber: number
): Promise<Submission | null> {
return this.findOne({ return this.findOne({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,
@ -14,7 +17,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
}); });
} }
public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> { public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
return this.findOne( return this.findOne(
{ {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -26,7 +29,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
); );
} }
public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> { public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
return this.findOne( return this.findOne(
{ {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -38,15 +41,15 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
); );
} }
public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> { public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
return this.find({ onBehalfOf: group }); return this.find({ onBehalfOf: group });
} }
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
return this.find({ submitter: student }); return this.find({ submitter: student });
} }
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> { public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,

View file

@ -4,13 +4,13 @@ import { ClassJoinRequest } from '../../entities/classes/class-join-request.enti
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> { export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { requester: requester } }); return this.findAll({ where: { requester: requester } });
} }
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { class: clazz } }); return this.findAll({ where: { class: clazz } });
} }
public deleteBy(requester: Student, clazz: Class): Promise<void> { public async deleteBy(requester: Student, clazz: Class): Promise<void> {
return this.deleteWhere({ requester: requester, class: clazz }); return this.deleteWhere({ requester: requester, class: clazz });
} }
} }

View file

@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js';
import { Teacher } from '../../entities/users/teacher.entity'; import { Teacher } from '../../entities/users/teacher.entity';
export class ClassRepository extends DwengoEntityRepository<Class> { export class ClassRepository extends DwengoEntityRepository<Class> {
public findById(id: string): Promise<Class | null> { public async findById(id: string): Promise<Class | null> {
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
} }
public deleteById(id: string): Promise<void> { public async deleteById(id: string): Promise<void> {
return this.deleteWhere({ classId: id }); return this.deleteWhere({ classId: id });
} }
public findByStudent(student: Student): Promise<Class[]> { public async findByStudent(student: Student): Promise<Class[]> {
return this.find( return this.find(
{ students: student }, { students: student },
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
); );
} }
public findByTeacher(teacher: Teacher): Promise<Class[]> { public async findByTeacher(teacher: Teacher): Promise<Class[]> {
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
} }
} }

View file

@ -4,16 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> { export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> { public async findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
return this.findAll({ where: { class: clazz } }); return this.findAll({ where: { class: clazz } });
} }
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> { public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
return this.findAll({ where: { sender: sender } }); return this.findAll({ where: { sender: sender } });
} }
public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> { public async findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
return this.findAll({ where: { receiver: receiver } }); return this.findAll({ where: { receiver: receiver } });
} }
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> { public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
sender: sender, sender: sender,
receiver: receiver, receiver: receiver,

View file

@ -4,7 +4,7 @@ import { Language } from '../../entities/content/language';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
export class AttachmentRepository extends DwengoEntityRepository<Attachment> { export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> { public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
return this.findOne({ return this.findOne({
learningObject: { learningObject: {
hruid: learningObjectId.hruid, hruid: learningObjectId.hruid,
@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
}); });
} }
public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise<Attachment | null> { public async findByMostRecentVersionOfLearningObjectAndName(
hruid: string,
language: Language,
attachmentName: string
): Promise<Attachment | null> {
return this.findOne( return this.findOne(
{ {
learningObject: { learningObject: {

View file

@ -5,7 +5,7 @@ import { Language } from '../../entities/content/language.js';
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
return this.findOne( return this.findOne(
{ {
hruid: identifier.hruid, hruid: identifier.hruid,
@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
); );
} }
public findLatestByHruidAndLanguage(hruid: string, language: Language) { public async findLatestByHruidAndLanguage(hruid: string, language: Language): Promise<LearningObject | null> {
return this.findOne( return this.findOne(
{ {
hruid: hruid, hruid: hruid,
@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
); );
} }
public findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> { public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
return this.find( return this.find(
{ admins: teacher }, { admins: teacher },
{ populate: ['admins'] } // Make sure to load admin relations { populate: ['admins'] } // Make sure to load admin relations

View file

@ -3,7 +3,7 @@ import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from '../../entities/content/language.js'; import { Language } from '../../entities/content/language.js';
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
} }

View file

@ -1,12 +1,12 @@
import { EntityRepository, FilterQuery } from '@mikro-orm/core'; import { EntityRepository, FilterQuery } from '@mikro-orm/core';
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
public async save(entity: T) { public async save(entity: T): Promise<void> {
const em = this.getEntityManager(); const em = this.getEntityManager();
em.persist(entity); em.persist(entity);
await em.flush(); await em.flush();
} }
public async deleteWhere(query: FilterQuery<T>) { public async deleteWhere(query: FilterQuery<T>): Promise<void> {
const toDelete = await this.findOne(query); const toDelete = await this.findOne(query);
const em = this.getEntityManager(); const em = this.getEntityManager();
if (toDelete) { if (toDelete) {

View file

@ -4,7 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js';
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class AnswerRepository extends DwengoEntityRepository<Answer> { export class AnswerRepository extends DwengoEntityRepository<Answer> {
public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
const answerEntity = this.create({ const answerEntity = this.create({
toQuestion: answer.toQuestion, toQuestion: answer.toQuestion,
author: answer.author, author: answer.author,
@ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
}); });
return this.insert(answerEntity); return this.insert(answerEntity);
} }
public findAllAnswersToQuestion(question: Question): Promise<Answer[]> { public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
return this.findAll({ return this.findAll({
where: { toQuestion: question }, where: { toQuestion: question },
orderBy: { sequenceNumber: 'ASC' }, orderBy: { sequenceNumber: 'ASC' },
}); });
} }
public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
toQuestion: question, toQuestion: question,
sequenceNumber: sequenceNumber, sequenceNumber: sequenceNumber,

View file

@ -5,7 +5,7 @@ import { Student } from '../../entities/users/student.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
export class QuestionRepository extends DwengoEntityRepository<Question> { export class QuestionRepository extends DwengoEntityRepository<Question> {
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
const questionEntity = this.create({ const questionEntity = this.create({
learningObjectHruid: question.loId.hruid, learningObjectHruid: question.loId.hruid,
learningObjectLanguage: question.loId.language, learningObjectLanguage: question.loId.language,
@ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
questionEntity.content = question.content; questionEntity.content = question.content;
return this.insert(questionEntity); return this.insert(questionEntity);
} }
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
return this.findAll({ return this.findAll({
where: { where: {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -33,7 +33,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
}, },
}); });
} }
public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> { public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,

View file

@ -34,7 +34,7 @@ let entityManager: EntityManager | undefined;
/** /**
* Execute all the database operations within the function f in a single transaction. * Execute all the database operations within the function f in a single transaction.
*/ */
export function transactional<T>(f: () => Promise<T>) { export function transactional<T>(f: () => Promise<T>): void {
entityManager?.transactional(f); entityManager?.transactional(f);
} }

View file

@ -2,10 +2,10 @@ import { Student } from '../../entities/users/student.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
export class StudentRepository extends DwengoEntityRepository<Student> { export class StudentRepository extends DwengoEntityRepository<Student> {
public findByUsername(username: string): Promise<Student | null> { public async findByUsername(username: string): Promise<Student | null> {
return this.findOne({ username: username }); return this.findOne({ username: username });
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username }); return this.deleteWhere({ username: username });
} }
} }

View file

@ -2,10 +2,10 @@ import { Teacher } from '../../entities/users/teacher.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
export class TeacherRepository extends DwengoEntityRepository<Teacher> { export class TeacherRepository extends DwengoEntityRepository<Teacher> {
public findByUsername(username: string): Promise<Teacher | null> { public async findByUsername(username: string): Promise<Teacher | null> {
return this.findOne({ username: username }); return this.findOne({ username: username });
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username }); return this.deleteWhere({ username: username });
} }
} }

View file

@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { User } from '../../entities/users/user.entity.js'; import { User } from '../../entities/users/user.entity.js';
export class UserRepository<T extends User> extends DwengoEntityRepository<T> { export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
public findByUsername(username: string): Promise<T | null> { public async findByUsername(username: string): Promise<T | null> {
return this.findOne({ username } as Partial<T>); return this.findOne({ username } as Partial<T>);
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username } as Partial<T>); return this.deleteWhere({ username } as Partial<T>);
} }
} }

View file

@ -5,5 +5,7 @@ export class LearningObjectIdentifier {
public hruid: string, public hruid: string,
public language: Language, public language: Language,
public version: number public version: number
) {} ) {
// Do nothing
}
} }

View file

@ -28,7 +28,7 @@ function initializeLogger(): Logger {
level: LOG_LEVEL, level: LOG_LEVEL,
json: true, json: true,
format: format.combine(format.timestamp(), format.json()), format: format.combine(format.timestamp(), format.json()),
onConnectionError: (err) => { onConnectionError: (err): void => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`Connection error: ${err}`); console.error(`Connection error: ${err}`);
}, },

View file

@ -5,7 +5,7 @@ import { LokiLabels } from 'loki-logger-ts';
export class MikroOrmLogger extends DefaultLogger { export class MikroOrmLogger extends DefaultLogger {
private logger: Logger = getLogger(); private logger: Logger = getLogger();
log(namespace: LoggerNamespace, message: string, context?: LogContext) { log(namespace: LoggerNamespace, message: string, context?: LogContext): void {
if (!this.isEnabled(namespace, context)) { if (!this.isEnabled(namespace, context)) {
return; return;
} }
@ -48,7 +48,7 @@ export class MikroOrmLogger extends DefaultLogger {
} }
} }
private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) { private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown {
const labels: LokiLabels = { const labels: LokiLabels = {
service: 'ORM', service: 'ORM',
}; };

View file

@ -1,7 +1,7 @@
import { getLogger, Logger } from './initalize.js'; import { getLogger, Logger } from './initalize.js';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
export function responseTimeLogger(req: Request, res: Response, time: number) { export function responseTimeLogger(req: Request, res: Response, time: number): void {
const logger: Logger = getLogger(); const logger: Logger = getLogger();
const method = req.method; const method = req.method;

View file

@ -1,9 +1,9 @@
import { envVars, getEnvVar } from '../../util/envVars.js'; import { envVars, getEnvVar } from '../../util/envVars.js';
import { expressjwt } from 'express-jwt'; import { expressjwt } from 'express-jwt';
import * as jwt from 'jsonwebtoken';
import { JwtPayload } from 'jsonwebtoken'; import { JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa'; import jwksClient from 'jwks-rsa';
import * as express from 'express'; import * as express from 'express';
import * as jwt from 'jsonwebtoken';
import { AuthenticatedRequest } from './authenticated-request.js'; import { AuthenticatedRequest } from './authenticated-request.js';
import { AuthenticationInfo } from './authentication-info.js'; import { AuthenticationInfo } from './authentication-info.js';
import { ForbiddenException, UnauthorizedException } from '../../exceptions.js'; import { ForbiddenException, UnauthorizedException } from '../../exceptions.js';
@ -74,7 +74,7 @@ const verifyJwtToken = expressjwt({
*/ */
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
if (!req.jwtPayload) { if (!req.jwtPayload) {
return; return undefined;
} }
const issuer = req.jwtPayload.iss; const issuer = req.jwtPayload.iss;
let accountType: 'student' | 'teacher'; let accountType: 'student' | 'teacher';
@ -84,8 +84,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
} else if (issuer === idpConfigs.teacher.issuer) { } else if (issuer === idpConfigs.teacher.issuer) {
accountType = 'teacher'; accountType = 'teacher';
} else { } else {
return; return undefined;
} }
return { return {
accountType: accountType, accountType: accountType,
username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!,
@ -100,10 +101,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
* Add the AuthenticationInfo object with the information about the current authentication to the request in order * Add the AuthenticationInfo object with the information about the current authentication to the request in order
* to avoid that the routers have to deal with the JWT token. * to avoid that the routers have to deal with the JWT token.
*/ */
const addAuthenticationInfo = (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction) => { function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void {
req.auth = getAuthenticationInfo(req); req.auth = getAuthenticationInfo(req);
next(); next();
}; }
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
@ -113,9 +114,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
* to true. * to true.
*/ */
export const authorize = export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
(accessCondition: (auth: AuthenticationInfo) => boolean) => return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
if (!req.auth) { if (!req.auth) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} else if (!accessCondition(req.auth)) { } else if (!accessCondition(req.auth)) {
@ -124,6 +124,7 @@ export const authorize =
next(); next();
} }
}; };
}
/** /**
* Middleware which rejects all unauthenticated users, but accepts all authenticated users. * Middleware which rejects all unauthenticated users, but accepts all authenticated users.

View file

@ -54,7 +54,7 @@ function config(testingMode: boolean = false): Options {
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION) // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint) // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
dynamicImportProvider: (id) => import(id), dynamicImportProvider: async (id) => import(id),
}; };
} }

View file

@ -4,7 +4,7 @@ import { envVars, getEnvVar } from './util/envVars.js';
import { getLogger, Logger } from './logging/initalize.js'; import { getLogger, Logger } from './logging/initalize.js';
let orm: MikroORM | undefined; let orm: MikroORM | undefined;
export async function initORM(testingMode: boolean = false) { export async function initORM(testingMode: boolean = false): Promise<void> {
const logger: Logger = getLogger(); const logger: Logger = getLogger();
logger.info('Initializing ORM'); logger.info('Initializing ORM');

View file

@ -79,7 +79,7 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe
const groups = await groupRepository.findAllGroupsForAssignment(assignment); const groups = await groupRepository.findAllGroupsForAssignment(assignment);
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
return submissions.map(mapToSubmissionDTO); return submissions.map(mapToSubmissionDTO);
} }

View file

@ -24,11 +24,15 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
export async function createClass(classData: ClassDTO): Promise<Class | null> { export async function createClass(classData: ClassDTO): Promise<Class | null> {
const teacherRepository = getTeacherRepository(); const teacherRepository = getTeacherRepository();
const teacherUsernames = classData.teachers || []; const teacherUsernames = classData.teachers || [];
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher != null); const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter(
(teacher) => teacher != null
);
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const studentUsernames = classData.students || []; const studentUsernames = classData.students || [];
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student != null
);
//Const cls = mapToClass(classData, teachers, students); //Const cls = mapToClass(classData, teachers, students);

View file

@ -43,7 +43,9 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student != null
);
getLogger().debug(members); getLogger().debug(members);

View file

@ -3,7 +3,7 @@ import { Attachment } from '../../entities/content/attachment.entity.js';
import { LearningObjectIdentifier } from '../../interfaces/learning-content.js'; import { LearningObjectIdentifier } from '../../interfaces/learning-content.js';
const attachmentService = { const attachmentService = {
getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
const attachmentRepo = getAttachmentRepository(); const attachmentRepo = getAttachmentRepository();
if (learningObjectId.version) { if (learningObjectId.version) {

View file

@ -41,7 +41,7 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
}; };
} }
function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> { async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language);
@ -69,7 +69,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
if (!learningObject) { if (!learningObject) {
return null; return null;
} }
return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id)); return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id));
}, },
/** /**
@ -96,7 +96,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
throw new NotFoundError('The learning path with the given ID could not be found.'); throw new NotFoundError('The learning path with the given ID could not be found.');
} }
const learningObjects = await Promise.all( const learningObjects = await Promise.all(
learningPath.nodes.map((it) => { learningPath.nodes.map(async (it) => {
const learningObject = learningObjectService.getLearningObjectById({ const learningObject = learningObjectService.getLearningObjectById({
hruid: it.learningObjectHruid, hruid: it.learningObjectHruid,
language: it.language, language: it.language,

View file

@ -18,28 +18,28 @@ const learningObjectService = {
/** /**
* Fetches a single learning object by its HRUID * Fetches a single learning object by its HRUID
*/ */
getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
return getProvider(id).getLearningObjectById(id); return getProvider(id).getLearningObjectById(id);
}, },
/** /**
* Fetch full learning object data (metadata) * Fetch full learning object data (metadata)
*/ */
getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
return getProvider(id).getLearningObjectsFromPath(id); return getProvider(id).getLearningObjectsFromPath(id);
}, },
/** /**
* Fetch only learning object HRUIDs * Fetch only learning object HRUIDs
*/ */
getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> { async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
return getProvider(id).getLearningObjectIdsFromPath(id); return getProvider(id).getLearningObjectIdsFromPath(id);
}, },
/** /**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string). * Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/ */
getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
return getProvider(id).getLearningObjectHTML(id); return getProvider(id).getLearningObjectHTML(id);
}, },
}; };

View file

@ -15,7 +15,7 @@ class ExternProcessor extends StringProcessor {
super(DwengoContentType.EXTERN); super(DwengoContentType.EXTERN);
} }
override renderFn(externURL: string) { override renderFn(externURL: string): string {
if (!isValidHttpUrl(externURL)) { if (!isValidHttpUrl(externURL)) {
throw new ProcessingError('The url is not valid: ' + externURL); throw new ProcessingError('The url is not valid: ' + externURL);
} }

View file

@ -32,7 +32,7 @@ class GiftProcessor extends StringProcessor {
super(DwengoContentType.GIFT); super(DwengoContentType.GIFT);
} }
override renderFn(giftString: string) { override renderFn(giftString: string): string {
const quizQuestions: GIFTQuestion[] = parse(giftString); const quizQuestions: GIFTQuestion[] = parse(giftString);
let html = "<div class='learning-object-gift'>\n"; let html = "<div class='learning-object-gift'>\n";

View file

@ -10,7 +10,7 @@ class BlockImageProcessor extends InlineImageProcessor {
super(); super();
} }
override renderFn(imageUrl: string) { override renderFn(imageUrl: string): string {
const inlineHtml = super.render(imageUrl); const inlineHtml = super.render(imageUrl);
return DOMPurify.sanitize(`<div>${inlineHtml}</div>`); return DOMPurify.sanitize(`<div>${inlineHtml}</div>`);
} }

View file

@ -13,7 +13,7 @@ class InlineImageProcessor extends StringProcessor {
super(contentType); super(contentType);
} }
override renderFn(imageUrl: string) { override renderFn(imageUrl: string): string {
if (!isValidHttpUrl(imageUrl)) { if (!isValidHttpUrl(imageUrl)) {
throw new ProcessingError(`Image URL is invalid: ${imageUrl}`); throw new ProcessingError(`Image URL is invalid: ${imageUrl}`);
} }

View file

@ -14,7 +14,7 @@ class MarkdownProcessor extends StringProcessor {
super(DwengoContentType.TEXT_MARKDOWN); super(DwengoContentType.TEXT_MARKDOWN);
} }
override renderFn(mdText: string) { override renderFn(mdText: string): string {
try { try {
marked.use({ renderer: dwengoMarkedRenderer }); marked.use({ renderer: dwengoMarkedRenderer });
const html = marked(mdText, { async: false }); const html = marked(mdText, { async: false });
@ -24,7 +24,7 @@ class MarkdownProcessor extends StringProcessor {
} }
} }
replaceLinks(html: string) { replaceLinks(html: string): string {
const proc = new InlineImageProcessor(); const proc = new InlineImageProcessor();
html = html.replace( html = html.replace(
/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g, /<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g,

View file

@ -15,7 +15,7 @@ class PdfProcessor extends StringProcessor {
super(DwengoContentType.APPLICATION_PDF); super(DwengoContentType.APPLICATION_PDF);
} }
override renderFn(pdfUrl: string) { override renderFn(pdfUrl: string): string {
if (!isValidHttpUrl(pdfUrl)) { if (!isValidHttpUrl(pdfUrl)) {
throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`); throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`);
} }

View file

@ -9,7 +9,9 @@ import { DwengoContentType } from './content-type.js';
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processor.js * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processor.js
*/ */
abstract class Processor<T> { abstract class Processor<T> {
protected constructor(public contentType: DwengoContentType) {} protected constructor(public contentType: DwengoContentType) {
// Do nothing
}
/** /**
* Render the given object. * Render the given object.

View file

@ -11,7 +11,7 @@ class TextProcessor extends StringProcessor {
super(DwengoContentType.TEXT_PLAIN); super(DwengoContentType.TEXT_PLAIN);
} }
override renderFn(text: string) { override renderFn(text: string): string {
// Sanitize plain text to prevent xss. // Sanitize plain text to prevent xss.
return DOMPurify.sanitize(text); return DOMPurify.sanitize(text);
} }

View file

@ -18,7 +18,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
// Its corresponding learning object. // Its corresponding learning object.
const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>(
await Promise.all( await Promise.all(
nodes.map((node) => nodes.map(async (node) =>
learningObjectService learningObjectService
.getLearningObjectById({ .getLearningObjectById({
hruid: node.learningObjectHruid, hruid: node.learningObjectHruid,
@ -117,7 +117,7 @@ async function convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>, nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget personalizedFor?: PersonalizationTarget
): Promise<LearningObjectNode[]> { ): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map((entry) => const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) =>
convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects) convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects)
); );
return await Promise.all(nodesPromise); return await Promise.all(nodesPromise);
@ -179,11 +179,11 @@ const databaseLearningPathProvider: LearningPathProvider = {
): Promise<LearningPathResponse> { ): Promise<LearningPathResponse> {
const learningPathRepo = getLearningPathRepository(); const learningPathRepo = getLearningPathRepository();
const learningPaths = (await Promise.all(hruids.map((hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter( const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter(
(learningPath) => learningPath !== null (learningPath) => learningPath !== null
); );
const filteredLearningPaths = await Promise.all( const filteredLearningPaths = await Promise.all(
learningPaths.map((learningPath, index) => convertLearningPath(learningPath, index, personalizedFor)) learningPaths.map(async (learningPath, index) => convertLearningPath(learningPath, index, personalizedFor))
); );
return { return {
@ -200,7 +200,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
const learningPathRepo = getLearningPathRepository(); const learningPathRepo = getLearningPathRepository();
const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language);
return await Promise.all(searchResults.map((result, index) => convertLearningPath(result, index, personalizedFor))); return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor)));
}, },
}; };

View file

@ -49,7 +49,9 @@ const learningPathService = {
* Search learning paths in the data source using the given search string. * Search learning paths in the data source using the given search string.
*/ */
async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> { async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> {
const providerResponses = await Promise.all(allProviders.map((provider) => provider.searchLearningPaths(query, language, personalizedFor))); const providerResponses = await Promise.all(
allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor))
);
return providerResponses.flat(); return providerResponses.flat();
}, },
}; };

View file

@ -2,7 +2,7 @@ import { getAnswerRepository, getQuestionRepository } from '../data/repositories
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
import { Question } from '../entities/questions/question.entity.js'; import { Question } from '../entities/questions/question.entity.js';
import { Answer } from '../entities/questions/answer.entity.js'; import { Answer } from '../entities/questions/answer.entity.js';
import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js'; import { AnswerDTO, AnswerId, mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js';
import { QuestionRepository } from '../data/questions/question-repository.js'; import { QuestionRepository } from '../data/questions/question-repository.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToStudent } from '../interfaces/student.js'; import { mapToStudent } from '../interfaces/student.js';
@ -45,7 +45,7 @@ export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO |
return mapToQuestionDTO(question); return mapToQuestionDTO(question);
} }
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) { export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> {
const answerRepository = getAnswerRepository(); const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId); const question = await fetchQuestion(questionId);
@ -68,7 +68,7 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean
return answersDTO.map(mapToAnswerId); return answersDTO.map(mapToAnswerId);
} }
export async function createQuestion(questionDTO: QuestionDTO) { export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> {
const questionRepository = getQuestionRepository(); const questionRepository = getQuestionRepository();
const author = mapToStudent(questionDTO.author); const author = mapToStudent(questionDTO.author);
@ -86,7 +86,7 @@ export async function createQuestion(questionDTO: QuestionDTO) {
return questionDTO; return questionDTO;
} }
export async function deleteQuestion(questionId: QuestionId) { export async function deleteQuestion(questionId: QuestionId): Promise<Question | null> {
const questionRepository = getQuestionRepository(); const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId); const question = await fetchQuestion(questionId);

View file

@ -2,6 +2,7 @@ import { getSubmissionRepository } from '../data/repositories.js';
import { Language } from '../entities/content/language.js'; import { Language } from '../entities/content/language.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
import { Submission } from '../entities/assignments/submission.entity.js';
export async function getSubmission( export async function getSubmission(
learningObjectHruid: string, learningObjectHruid: string,
@ -21,7 +22,7 @@ export async function getSubmission(
return mapToSubmissionDTO(submission); return mapToSubmissionDTO(submission);
} }
export async function createSubmission(submissionDTO: SubmissionDTO) { export async function createSubmission(submissionDTO: SubmissionDTO): Promise<Submission | null> {
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO); const submission = mapToSubmission(submissionDTO);
@ -35,7 +36,12 @@ export async function createSubmission(submissionDTO: SubmissionDTO) {
return submission; return submission;
} }
export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) { export async function deleteSubmission(
learningObjectHruid: string,
language: Language,
version: number,
submissionNumber: number
): Promise<SubmissionDTO | null> {
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submission = getSubmission(learningObjectHruid, language, version, submissionNumber); const submission = getSubmission(learningObjectHruid, language, version, submissionNumber);

View file

@ -77,7 +77,7 @@ export async function getClassIdsByTeacher(username: string): Promise<string[]>
return classes.map((cls) => cls.id); return classes.map((cls) => cls.id);
} }
export async function fetchStudentsByTeacher(username: string) { export async function fetchStudentsByTeacher(username: string): Promise<StudentDTO[]> {
const classes = await getClassIdsByTeacher(username); const classes = await getClassIdsByTeacher(username);
return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat();

View file

@ -6,7 +6,11 @@
* @param regex * @param regex
* @param replacementFn * @param replacementFn
*/ */
export async function replaceAsync(str: string, regex: RegExp, replacementFn: (match: string, ...args: string[]) => Promise<string>) { export async function replaceAsync(
str: string,
regex: RegExp,
replacementFn: (match: string, ...args: string[]) => Promise<string>
): Promise<string> {
const promises: Promise<string>[] = []; const promises: Promise<string>[] = [];
// First run through matches: add all Promises resulting from the replacement function // First run through matches: add all Promises resulting from the replacement function

View file

@ -9,7 +9,7 @@ export function isValidHttpUrl(url: string): boolean {
} }
} }
export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifier) { export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifier): string {
let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`; let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`;
if (learningObjectId.version) { if (learningObjectId.version) {
url += `&version=${learningObjectId.version}`; url += `&version=${learningObjectId.version}`;

View file

@ -78,7 +78,7 @@ describe('DatabaseLearningObjectProvider', () => {
}); });
it('should throw an error if queried with a path identifier for which there is no learning path', async () => { it('should throw an error if queried with a path identifier for which there is no learning path', async () => {
await expect( await expect(
(async () => { (async (): Promise<void> => {
await databaseLearningObjectProvider.getLearningObjectIdsFromPath({ await databaseLearningObjectProvider.getLearningObjectIdsFromPath({
hruid: 'non_existing_hruid', hruid: 'non_existing_hruid',
language: Language.Dutch, language: Language.Dutch,
@ -97,7 +97,7 @@ describe('DatabaseLearningObjectProvider', () => {
}); });
it('should throw an error if queried with a path identifier for which there is no learning path', async () => { it('should throw an error if queried with a path identifier for which there is no learning path', async () => {
await expect( await expect(
(async () => { (async (): Promise<void> => {
await databaseLearningObjectProvider.getLearningObjectsFromPath({ await databaseLearningObjectProvider.getLearningObjectsFromPath({
hruid: 'non_existing_hruid', hruid: 'non_existing_hruid',
language: Language.Dutch, language: Language.Dutch,

View file

@ -124,7 +124,7 @@ describe('DatabaseLearningPathProvider', () => {
const learningObjectsOnPath = ( const learningObjectsOnPath = (
await Promise.all( await Promise.all(
example.learningPath.nodes.map((node) => example.learningPath.nodes.map(async (node) =>
learningObjectService.getLearningObjectById({ learningObjectService.getLearningObjectById({
hruid: node.learningObjectHruid, hruid: node.learningObjectHruid,
version: node.version, version: node.version,

View file

@ -14,7 +14,7 @@ import { makeTestQuestions } from './test_assets/questions/questions.testdata.js
import { makeTestAnswers } from './test_assets/questions/answers.testdata.js'; import { makeTestAnswers } from './test_assets/questions/answers.testdata.js';
import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js'; import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js';
export async function setupTestApp() { export async function setupTestApp(): Promise<void> {
dotenv.config({ path: '.env.test' }); dotenv.config({ path: '.env.test' });
await initORM(true); await initORM(true);

View file

@ -11,7 +11,7 @@ import { envVars, getEnvVar } from '../../../../src/util/envVars';
*/ */
export function dummyLearningObject(hruid: string, language: Language, title: string): LearningObjectExample { export function dummyLearningObject(hruid: string, language: Language, title: string): LearningObjectExample {
return { return {
createLearningObject: () => { createLearningObject: (): LearningObject => {
const learningObject = new LearningObject(); const learningObject = new LearningObject();
learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + hruid; learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + hruid;
learningObject.language = language; learningObject.language = language;

View file

@ -3,7 +3,12 @@ import { LearningPathTransition } from '../../../src/entities/content/learning-p
import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity';
import { LearningPath } from '../../../src/entities/content/learning-path.entity'; import { LearningPath } from '../../../src/entities/content/learning-path.entity';
export function createLearningPathTransition(node: LearningPathNode, transitionNumber: number, condition: string | null, to: LearningPathNode) { export function createLearningPathTransition(
node: LearningPathNode,
transitionNumber: number,
condition: string | null,
to: LearningPathNode
): LearningPathTransition {
const trans = new LearningPathTransition(); const trans = new LearningPathTransition();
trans.node = node; trans.node = node;
trans.transitionNumber = transitionNumber; trans.transitionNumber = transitionNumber;
@ -19,7 +24,7 @@ export function createLearningPathNode(
version: number, version: number,
language: Language, language: Language,
startNode: boolean startNode: boolean
) { ): LearningPathNode {
const node = new LearningPathNode(); const node = new LearningPathNode();
node.learningPath = learningPath; node.learningPath = learningPath;
node.nodeNumber = nodeNumber; node.nodeNumber = nodeNumber;

View file

@ -69,7 +69,7 @@ export function expectToBeCorrectEntity<T extends object>(actual: { entity: T; n
* @param filtered the representation as FilteredLearningObject * @param filtered the representation as FilteredLearningObject
* @param original the original entity added to the database * @param original the original entity added to the database
*/ */
export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearningObject, original: LearningObject) { export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearningObject, original: LearningObject): void {
expect(filtered.uuid).toEqual(original.uuid); expect(filtered.uuid).toEqual(original.uuid);
expect(filtered.version).toEqual(original.version); expect(filtered.version).toEqual(original.version);
expect(filtered.language).toEqual(original.language); expect(filtered.language).toEqual(original.language);
@ -105,7 +105,7 @@ export function expectToBeCorrectLearningPath(
learningPath: LearningPath, learningPath: LearningPath,
expectedEntity: LearningPathEntity, expectedEntity: LearningPathEntity,
learningObjectsOnPath: FilteredLearningObject[] learningObjectsOnPath: FilteredLearningObject[]
) { ): void {
expect(learningPath.hruid).toEqual(expectedEntity.hruid); expect(learningPath.hruid).toEqual(expectedEntity.hruid);
expect(learningPath.language).toEqual(expectedEntity.language); expect(learningPath.language).toEqual(expectedEntity.language);
expect(learningPath.description).toEqual(expectedEntity.description); expect(learningPath.description).toEqual(expectedEntity.description);

View file

@ -23,11 +23,20 @@ export default [
languageOptions: { languageOptions: {
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
}, },
linterOptions: { linterOptions: {
reportUnusedInlineConfigs: 'error', reportUnusedInlineConfigs: 'error',
}, },
rules: { rules: {
'consistent-return': 'off',
'@typescript-eslint/consistent-return': 'off',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/naming-convention': [ '@typescript-eslint/naming-convention': [
'warn', 'warn',
{ // Enforce that all variables, functions and properties are camelCase { // Enforce that all variables, functions and properties are camelCase
@ -49,6 +58,14 @@ export default [
} }
], ],
// 'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'no-loop-func': 'off',
'@typescript-eslint/no-loop-func': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'no-unused-expressions': 'off', 'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 'warn', '@typescript-eslint/no-unused-expressions': 'warn',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
@ -66,6 +83,10 @@ export default [
'no-use-before-define': 'off', 'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/promise-function-async': 'warn',
'no-await-in-loop': 'warn', 'no-await-in-loop': 'warn',
'no-constructor-return': 'error', 'no-constructor-return': 'error',
'no-duplicate-imports': 'error', 'no-duplicate-imports': 'error',
@ -110,7 +131,6 @@ export default [
'no-iterator': 'error', 'no-iterator': 'error',
'no-label-var': 'warn', 'no-label-var': 'warn',
'no-labels': 'warn', 'no-labels': 'warn',
'no-loop-func': 'error',
'no-multi-assign': 'error', 'no-multi-assign': 'error',
'no-nested-ternary': 'error', 'no-nested-ternary': 'error',
'no-object-constructor': 'error', 'no-object-constructor': 'error',