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
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
async function startServer() {
async function startServer(): Promise<void> {
await initORM();
app.listen(port, () => {

View file

@ -47,7 +47,7 @@ export async function getStudentHandler(req: Request, res: Response): Promise<vo
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;
if (!userData.username || !userData.firstName || !userData.lastName) {
@ -61,7 +61,7 @@ export async function createStudentHandler(req: Request, res: Response) {
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;
if (!username) {

View file

@ -30,7 +30,7 @@ export async function getSubmissionHandler(req: Request<SubmissionParams>, res:
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 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 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);
}
export async function createTeacherHandler(req: Request, res: Response) {
export async function createTeacherHandler(req: Request, res: Response): Promise<void> {
const userData = req.body as TeacherDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
@ -64,7 +64,7 @@ export async function createTeacherHandler(req: Request, res: Response) {
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;
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 translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => ({
@ -21,7 +21,7 @@ export function getThemes(req: Request, res: Response) {
res.json(themeList);
}
export function getThemeByTitle(req: Request, res: Response) {
export function getThemeByTitle(req: Request, res: Response): void {
const themeKey = req.params.theme;
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 {
getLogger().debug({ req: req });
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 {
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';
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 });
}
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
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 });
}
}

View file

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

View file

@ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
import { Student } from '../../entities/users/student.entity.js';
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({
learningObjectHruid: loId.hruid,
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(
{
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(
{
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 });
}
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
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({
learningObjectHruid: loId.hruid,
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';
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 } });
}
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
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 });
}
}

View file

@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js';
import { Teacher } from '../../entities/users/teacher.entity';
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'] });
}
public deleteById(id: string): Promise<void> {
public async deleteById(id: string): Promise<void> {
return this.deleteWhere({ classId: id });
}
public findByStudent(student: Student): Promise<Class[]> {
public async findByStudent(student: Student): Promise<Class[]> {
return this.find(
{ students: student },
{ 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'] });
}
}

View file

@ -4,16 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent
import { Teacher } from '../../entities/users/teacher.entity.js';
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 } });
}
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
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 } });
}
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
return this.deleteWhere({
sender: sender,
receiver: receiver,

View file

@ -4,7 +4,7 @@ import { Language } from '../../entities/content/language';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
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({
learningObject: {
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(
{
learningObject: {

View file

@ -5,7 +5,7 @@ import { Language } from '../../entities/content/language.js';
import { Teacher } from '../../entities/users/teacher.entity.js';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
return this.findOne(
{
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(
{
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(
{ admins: teacher },
{ 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';
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'] });
}

View file

@ -1,12 +1,12 @@
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
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();
em.persist(entity);
await em.flush();
}
public async deleteWhere(query: FilterQuery<T>) {
public async deleteWhere(query: FilterQuery<T>): Promise<void> {
const toDelete = await this.findOne(query);
const em = this.getEntityManager();
if (toDelete) {

View file

@ -4,7 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js';
import { Teacher } from '../../entities/users/teacher.entity.js';
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({
toQuestion: answer.toQuestion,
author: answer.author,
@ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
});
return this.insert(answerEntity);
}
public findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
return this.findAll({
where: { toQuestion: question },
orderBy: { sequenceNumber: 'ASC' },
});
}
public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
return this.deleteWhere({
toQuestion: question,
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';
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({
learningObjectHruid: question.loId.hruid,
learningObjectLanguage: question.loId.language,
@ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
questionEntity.content = question.content;
return this.insert(questionEntity);
}
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
return this.findAll({
where: {
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({
learningObjectHruid: loId.hruid,
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.
*/
export function transactional<T>(f: () => Promise<T>) {
export function transactional<T>(f: () => Promise<T>): void {
entityManager?.transactional(f);
}

View file

@ -2,10 +2,10 @@ import { Student } from '../../entities/users/student.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
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 });
}
public deleteByUsername(username: string): Promise<void> {
public async deleteByUsername(username: string): Promise<void> {
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';
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 });
}
public deleteByUsername(username: string): Promise<void> {
public async deleteByUsername(username: string): Promise<void> {
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';
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>);
}
public deleteByUsername(username: string): Promise<void> {
public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username } as Partial<T>);
}
}

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import { LokiLabels } from 'loki-logger-ts';
export class MikroOrmLogger extends DefaultLogger {
private logger: Logger = getLogger();
log(namespace: LoggerNamespace, message: string, context?: LogContext) {
log(namespace: LoggerNamespace, message: string, context?: LogContext): void {
if (!this.isEnabled(namespace, context)) {
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 = {
service: 'ORM',
};

View file

@ -1,7 +1,7 @@
import { getLogger, Logger } from './initalize.js';
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 method = req.method;

View file

@ -1,9 +1,9 @@
import { envVars, getEnvVar } from '../../util/envVars.js';
import { expressjwt } from 'express-jwt';
import * as jwt from 'jsonwebtoken';
import { JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import * as express from 'express';
import * as jwt from 'jsonwebtoken';
import { AuthenticatedRequest } from './authenticated-request.js';
import { AuthenticationInfo } from './authentication-info.js';
import { ForbiddenException, UnauthorizedException } from '../../exceptions.js';
@ -74,7 +74,7 @@ const verifyJwtToken = expressjwt({
*/
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
if (!req.jwtPayload) {
return;
return undefined;
}
const issuer = req.jwtPayload.iss;
let accountType: 'student' | 'teacher';
@ -84,8 +84,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
} else if (issuer === idpConfigs.teacher.issuer) {
accountType = 'teacher';
} else {
return;
return undefined;
}
return {
accountType: accountType,
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
* 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);
next();
};
}
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
* to true.
*/
export const authorize =
(accessCondition: (auth: AuthenticationInfo) => boolean) =>
(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
if (!req.auth) {
throw new UnauthorizedException();
} else if (!accessCondition(req.auth)) {
@ -124,6 +124,7 @@ export const authorize =
next();
}
};
}
/**
* 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)
// (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';
let orm: MikroORM | undefined;
export async function initORM(testingMode: boolean = false) {
export async function initORM(testingMode: boolean = false): Promise<void> {
const logger: Logger = getLogger();
logger.info('Initializing ORM');

View file

@ -79,7 +79,7 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
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);
}

View file

@ -24,11 +24,15 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
export async function createClass(classData: ClassDTO): Promise<Class | null> {
const teacherRepository = getTeacherRepository();
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 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);

View file

@ -43,7 +43,9 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
const studentRepository = getStudentRepository();
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);

View file

@ -3,7 +3,7 @@ import { Attachment } from '../../entities/content/attachment.entity.js';
import { LearningObjectIdentifier } from '../../interfaces/learning-content.js';
const attachmentService = {
getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
const attachmentRepo = getAttachmentRepository();
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();
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language);
@ -69,7 +69,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
if (!learningObject) {
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.');
}
const learningObjects = await Promise.all(
learningPath.nodes.map((it) => {
learningPath.nodes.map(async (it) => {
const learningObject = learningObjectService.getLearningObjectById({
hruid: it.learningObjectHruid,
language: it.language,

View file

@ -18,28 +18,28 @@ const learningObjectService = {
/**
* 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);
},
/**
* Fetch full learning object data (metadata)
*/
getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
return getProvider(id).getLearningObjectsFromPath(id);
},
/**
* Fetch only learning object HRUIDs
*/
getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
return getProvider(id).getLearningObjectIdsFromPath(id);
},
/**
* 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);
},
};

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ class MarkdownProcessor extends StringProcessor {
super(DwengoContentType.TEXT_MARKDOWN);
}
override renderFn(mdText: string) {
override renderFn(mdText: string): string {
try {
marked.use({ renderer: dwengoMarkedRenderer });
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();
html = html.replace(
/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g,

View file

@ -15,7 +15,7 @@ class PdfProcessor extends StringProcessor {
super(DwengoContentType.APPLICATION_PDF);
}
override renderFn(pdfUrl: string) {
override renderFn(pdfUrl: string): string {
if (!isValidHttpUrl(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
*/
abstract class Processor<T> {
protected constructor(public contentType: DwengoContentType) {}
protected constructor(public contentType: DwengoContentType) {
// Do nothing
}
/**
* Render the given object.

View file

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

View file

@ -18,7 +18,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
// Its corresponding learning object.
const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>(
await Promise.all(
nodes.map((node) =>
nodes.map(async (node) =>
learningObjectService
.getLearningObjectById({
hruid: node.learningObjectHruid,
@ -117,7 +117,7 @@ async function convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget
): 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)
);
return await Promise.all(nodesPromise);
@ -179,11 +179,11 @@ const databaseLearningPathProvider: LearningPathProvider = {
): Promise<LearningPathResponse> {
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
);
const filteredLearningPaths = await Promise.all(
learningPaths.map((learningPath, index) => convertLearningPath(learningPath, index, personalizedFor))
learningPaths.map(async (learningPath, index) => convertLearningPath(learningPath, index, personalizedFor))
);
return {
@ -200,7 +200,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
const learningPathRepo = getLearningPathRepository();
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.
*/
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();
},
};

View file

@ -2,7 +2,7 @@ import { getAnswerRepository, getQuestionRepository } from '../data/repositories
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
import { Question } from '../entities/questions/question.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 { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToStudent } from '../interfaces/student.js';
@ -45,7 +45,7 @@ export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO |
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 question = await fetchQuestion(questionId);
@ -68,7 +68,7 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean
return answersDTO.map(mapToAnswerId);
}
export async function createQuestion(questionDTO: QuestionDTO) {
export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> {
const questionRepository = getQuestionRepository();
const author = mapToStudent(questionDTO.author);
@ -86,7 +86,7 @@ export async function createQuestion(questionDTO: QuestionDTO) {
return questionDTO;
}
export async function deleteQuestion(questionId: QuestionId) {
export async function deleteQuestion(questionId: QuestionId): Promise<Question | null> {
const questionRepository = getQuestionRepository();
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 { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
import { Submission } from '../entities/assignments/submission.entity.js';
export async function getSubmission(
learningObjectHruid: string,
@ -21,7 +22,7 @@ export async function getSubmission(
return mapToSubmissionDTO(submission);
}
export async function createSubmission(submissionDTO: SubmissionDTO) {
export async function createSubmission(submissionDTO: SubmissionDTO): Promise<Submission | null> {
const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO);
@ -35,7 +36,12 @@ export async function createSubmission(submissionDTO: SubmissionDTO) {
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 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);
}
export async function fetchStudentsByTeacher(username: string) {
export async function fetchStudentsByTeacher(username: string): Promise<StudentDTO[]> {
const classes = await getClassIdsByTeacher(username);
return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat();

View file

@ -6,7 +6,11 @@
* @param regex
* @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>[] = [];
// 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}`;
if (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 () => {
await expect(
(async () => {
(async (): Promise<void> => {
await databaseLearningObjectProvider.getLearningObjectIdsFromPath({
hruid: 'non_existing_hruid',
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 () => {
await expect(
(async () => {
(async (): Promise<void> => {
await databaseLearningObjectProvider.getLearningObjectsFromPath({
hruid: 'non_existing_hruid',
language: Language.Dutch,

View file

@ -124,7 +124,7 @@ describe('DatabaseLearningPathProvider', () => {
const learningObjectsOnPath = (
await Promise.all(
example.learningPath.nodes.map((node) =>
example.learningPath.nodes.map(async (node) =>
learningObjectService.getLearningObjectById({
hruid: node.learningObjectHruid,
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 { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js';
export async function setupTestApp() {
export async function setupTestApp(): Promise<void> {
dotenv.config({ path: '.env.test' });
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 {
return {
createLearningObject: () => {
createLearningObject: (): LearningObject => {
const learningObject = new LearningObject();
learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + hruid;
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 { 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();
trans.node = node;
trans.transitionNumber = transitionNumber;
@ -19,7 +24,7 @@ export function createLearningPathNode(
version: number,
language: Language,
startNode: boolean
) {
): LearningPathNode {
const node = new LearningPathNode();
node.learningPath = learningPath;
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 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.version).toEqual(original.version);
expect(filtered.language).toEqual(original.language);
@ -105,7 +105,7 @@ export function expectToBeCorrectLearningPath(
learningPath: LearningPath,
expectedEntity: LearningPathEntity,
learningObjectsOnPath: FilteredLearningObject[]
) {
): void {
expect(learningPath.hruid).toEqual(expectedEntity.hruid);
expect(learningPath.language).toEqual(expectedEntity.language);
expect(learningPath.description).toEqual(expectedEntity.description);

View file

@ -23,11 +23,20 @@ export default [
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
linterOptions: {
reportUnusedInlineConfigs: 'error',
},
rules: {
'consistent-return': 'off',
'@typescript-eslint/consistent-return': 'off',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/naming-convention': [
'warn',
{ // 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',
'@typescript-eslint/no-unused-expressions': 'warn',
'no-unused-vars': 'off',
@ -66,6 +83,10 @@ export default [
'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-constructor-return': 'error',
'no-duplicate-imports': 'error',
@ -110,7 +131,6 @@ export default [
'no-iterator': 'error',
'no-label-var': 'warn',
'no-labels': 'warn',
'no-loop-func': 'error',
'no-multi-assign': 'error',
'no-nested-ternary': 'error',
'no-object-constructor': 'error',