Merge branch 'dev' into refactor/linting
This commit is contained in:
commit
588c556949
37 changed files with 686 additions and 796 deletions
|
@ -9,6 +9,7 @@ import { envVars, getNumericEnvVar } from './util/envVars.js';
|
|||
import apiRouter from './routes/router.js';
|
||||
import swaggerMiddleware from './swagger.js';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import { errorHandler } from './middleware/error-handling/error-handler.js';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
|
@ -26,6 +27,8 @@ app.use('/api', apiRouter);
|
|||
// Swagger
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
async function startServer(): Promise<void> {
|
||||
await initORM();
|
||||
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { FALLBACK_LANG } from '../config.js';
|
||||
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js';
|
||||
import {
|
||||
FilteredLearningObject,
|
||||
LearningObjectIdentifier,
|
||||
LearningPathIdentifier,
|
||||
} from '../interfaces/learning-content.js';
|
||||
import learningObjectService from '../services/learning-objects/learning-object-service.js';
|
||||
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||
import { Language } from '../entities/content/language.js';
|
||||
import { BadRequestException } from '../exceptions/badRequestException.js';
|
||||
import attachmentService from '../services/learning-objects/attachment-service.js';
|
||||
import { NotFoundError } from '@mikro-orm/core';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
|
||||
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
|
||||
if (!req.params.hruid) {
|
||||
|
|
|
@ -2,14 +2,14 @@ import { Request, Response } from 'express';
|
|||
import { themes } from '../data/themes.js';
|
||||
import { FALLBACK_LANG } from '../config.js';
|
||||
import learningPathService from '../services/learning-paths/learning-path-service.js';
|
||||
import { BadRequestException } from '../exceptions/badRequestException.js';
|
||||
import { Language } from '../entities/content/language.js';
|
||||
import {
|
||||
PersonalizationTarget,
|
||||
personalizedForGroup,
|
||||
personalizedForStudent,
|
||||
} from '../services/learning-paths/learning-path-personalization-util.js';
|
||||
import { NotFoundException } from '../exceptions/notFoundException.js';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
|
||||
/**
|
||||
* Fetch learning paths based on query parameters.
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
||||
import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js';
|
||||
|
||||
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
|
||||
public async save(entity: T): Promise<void> {
|
||||
const em = this.getEntityManager();
|
||||
em.persist(entity);
|
||||
await em.flush();
|
||||
public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> {
|
||||
if (options?.preventOverwrite && (await this.findOne(entity))) {
|
||||
throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`);
|
||||
}
|
||||
await this.getEntityManager().persistAndFlush(entity);
|
||||
}
|
||||
public async deleteWhere(query: FilterQuery<T>): Promise<void> {
|
||||
const toDelete = await this.findOne(query);
|
||||
|
|
|
@ -13,12 +13,4 @@ export class Student extends User {
|
|||
|
||||
@ManyToMany(() => Group)
|
||||
groups!: Collection<Group>;
|
||||
|
||||
constructor(
|
||||
public username: string,
|
||||
public firstName: string,
|
||||
public lastName: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,4 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js';
|
|||
export class Teacher extends User {
|
||||
@ManyToMany(() => Class)
|
||||
classes!: Collection<Class>;
|
||||
|
||||
constructor(
|
||||
public username: string,
|
||||
public firstName: string,
|
||||
public lastName: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
|
10
backend/src/exceptions/bad-request-exception.ts
Normal file
10
backend/src/exceptions/bad-request-exception.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 400 Bad Request
|
||||
*/
|
||||
export class BadRequestException extends ExceptionWithHttpState {
|
||||
constructor(error: string) {
|
||||
super(400, error);
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { HttpException } from './httpException.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 400 Bad Request
|
||||
*/
|
||||
|
||||
export class BadRequestException extends HttpException {
|
||||
constructor(message = 'Bad Request') {
|
||||
super(400, message);
|
||||
}
|
||||
}
|
12
backend/src/exceptions/conflict-exception.ts
Normal file
12
backend/src/exceptions/conflict-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 409 Conflict
|
||||
*/
|
||||
export class ConflictException extends ExceptionWithHttpState {
|
||||
public status = 409;
|
||||
|
||||
constructor(error: string) {
|
||||
super(409, error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { ConflictException } from './conflict-exception.js';
|
||||
|
||||
export class EntityAlreadyExistsException extends ConflictException {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
11
backend/src/exceptions/exception-with-http-state.ts
Normal file
11
backend/src/exceptions/exception-with-http-state.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Exceptions which are associated with a HTTP error code.
|
||||
*/
|
||||
export abstract class ExceptionWithHttpState extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public error: string
|
||||
) {
|
||||
super(error);
|
||||
}
|
||||
}
|
12
backend/src/exceptions/forbidden-exception.ts
Normal file
12
backend/src/exceptions/forbidden-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 403 Forbidden
|
||||
*/
|
||||
export class ForbiddenException extends ExceptionWithHttpState {
|
||||
status = 403;
|
||||
|
||||
constructor(message = 'Forbidden') {
|
||||
super(403, message);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { HttpException } from './httpException.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 403 Forbidden
|
||||
*/
|
||||
export class ForbiddenException extends HttpException {
|
||||
constructor(message = 'Forbidden') {
|
||||
super(403, message);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export class HttpException extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
message: string
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
12
backend/src/exceptions/not-found-exception.ts
Normal file
12
backend/src/exceptions/not-found-exception.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 404 Not Found
|
||||
*/
|
||||
export class NotFoundException extends ExceptionWithHttpState {
|
||||
public status = 404;
|
||||
|
||||
constructor(error: string) {
|
||||
super(404, error);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { HttpException } from './httpException.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 404 Not Found
|
||||
*/
|
||||
export class NotFoundException extends HttpException {
|
||||
constructor(message = 'Not Found') {
|
||||
super(404, message);
|
||||
}
|
||||
}
|
10
backend/src/exceptions/unauthorized-exception.ts
Normal file
10
backend/src/exceptions/unauthorized-exception.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 401 Unauthorized
|
||||
*/
|
||||
export class UnauthorizedException extends ExceptionWithHttpState {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(401, message);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { HttpException } from './httpException.js';
|
||||
|
||||
/**
|
||||
* Exception for HTTP 401 Unauthorized
|
||||
*/
|
||||
export class UnauthorizedException extends HttpException {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(401, message);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { Student } from '../entities/users/student.entity.js';
|
||||
import { getStudentRepository } from '../data/repositories.js';
|
||||
|
||||
export interface StudentDTO {
|
||||
id: string;
|
||||
|
@ -23,7 +24,9 @@ export function mapToStudentDTO(student: Student): StudentDTO {
|
|||
}
|
||||
|
||||
export function mapToStudent(studentData: StudentDTO): Student {
|
||||
const student = new Student(studentData.username, studentData.firstName, studentData.lastName);
|
||||
|
||||
return student;
|
||||
return getStudentRepository().create({
|
||||
username: studentData.username,
|
||||
firstName: studentData.firstName,
|
||||
lastName: studentData.lastName,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||
import { getTeacherRepository } from '../data/repositories.js';
|
||||
|
||||
export interface TeacherDTO {
|
||||
id: string;
|
||||
|
@ -22,8 +23,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
|
|||
};
|
||||
}
|
||||
|
||||
export function mapToTeacher(teacherDTO: TeacherDTO): Teacher {
|
||||
const teacher = new Teacher(teacherDTO.username, teacherDTO.firstName, teacherDTO.lastName);
|
||||
|
||||
return teacher;
|
||||
export function mapToTeacher(teacherData: TeacherDTO): Teacher {
|
||||
return getTeacherRepository().create({
|
||||
username: teacherData.username,
|
||||
firstName: teacherData.firstName,
|
||||
lastName: teacherData.lastName,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import jwksClient from 'jwks-rsa';
|
|||
import * as express from 'express';
|
||||
import { AuthenticatedRequest } from './authenticated-request.js';
|
||||
import { AuthenticationInfo } from './authentication-info.js';
|
||||
import { UnauthorizedException } from '../../exceptions/unauthorizedException.js';
|
||||
import { ForbiddenException } from '../../exceptions/forbiddenException.js';
|
||||
import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
|
||||
import { ForbiddenException } from '../../exceptions/forbidden-exception.js';
|
||||
|
||||
const JWKS_CACHE = true;
|
||||
const JWKS_RATE_LIMIT = true;
|
||||
|
|
15
backend/src/middleware/error-handling/error-handler.ts
Normal file
15
backend/src/middleware/error-handling/error-handler.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import { getLogger, Logger } from '../../logging/initalize.js';
|
||||
import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-state.js';
|
||||
|
||||
const logger: Logger = getLogger();
|
||||
|
||||
export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void {
|
||||
if (err instanceof ExceptionWithHttpState) {
|
||||
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
|
||||
res.status(err.status).json(err);
|
||||
} else {
|
||||
logger.error(`Unexpected error occurred while handing a request: ${JSON.stringify(err)}`);
|
||||
res.status(500).json(err);
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@ function config(testingMode = false): Options {
|
|||
dbName: getEnvVar(envVars.DbName),
|
||||
subscribers: [new SqliteAutoincrementSubscriber()],
|
||||
entities: entities,
|
||||
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
|
||||
// EntitiesTs: entitiesTs,
|
||||
|
||||
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
|
||||
|
@ -65,6 +66,7 @@ function config(testingMode = false): Options {
|
|||
user: getEnvVar(envVars.DbUsername),
|
||||
password: getEnvVar(envVars.DbPassword),
|
||||
entities: entities,
|
||||
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
|
||||
// EntitiesTs: entitiesTs,
|
||||
|
||||
// Logging
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js';
|
||||
import {
|
||||
getClassRepository,
|
||||
getGroupRepository,
|
||||
getStudentRepository,
|
||||
getSubmissionRepository,
|
||||
} from '../data/repositories.js';
|
||||
import { AssignmentDTO } from '../interfaces/assignment.js';
|
||||
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
||||
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
||||
|
@ -27,15 +32,9 @@ export async function getStudent(username: string): Promise<StudentDTO | null> {
|
|||
export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> {
|
||||
const studentRepository = getStudentRepository();
|
||||
|
||||
try {
|
||||
const newStudent = studentRepository.create(mapToStudent(userData));
|
||||
await studentRepository.save(newStudent);
|
||||
|
||||
return mapToStudentDTO(newStudent);
|
||||
} catch (e) {
|
||||
getLogger().error(e);
|
||||
return null;
|
||||
}
|
||||
const newStudent = mapToStudent(userData);
|
||||
await studentRepository.save(newStudent, { preventOverwrite: true });
|
||||
return mapToStudentDTO(newStudent);
|
||||
}
|
||||
|
||||
export async function deleteStudent(username: string): Promise<StudentDTO | null> {
|
||||
|
|
|
@ -26,15 +26,10 @@ export async function getTeacher(username: string): Promise<TeacherDTO | null> {
|
|||
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> {
|
||||
const teacherRepository = getTeacherRepository();
|
||||
|
||||
try {
|
||||
const newTeacher = teacherRepository.create(mapToTeacher(userData));
|
||||
await teacherRepository.save(newTeacher);
|
||||
const newTeacher = mapToTeacher(userData);
|
||||
await teacherRepository.save(newTeacher, { preventOverwrite: true });
|
||||
|
||||
return mapToTeacherDTO(newTeacher);
|
||||
} catch (e) {
|
||||
getLogger().error(e);
|
||||
return null;
|
||||
}
|
||||
return mapToTeacherDTO(newTeacher);
|
||||
}
|
||||
|
||||
export async function deleteTeacher(username: string): Promise<TeacherDTO | null> {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { setupTestApp } from '../../setup-tests.js';
|
||||
import { Student } from '../../../src/entities/users/student.entity.js';
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { StudentRepository } from '../../../src/data/users/student-repository.js';
|
||||
import { getStudentRepository } from '../../../src/data/repositories.js';
|
||||
|
@ -30,7 +29,7 @@ describe('StudentRepository', () => {
|
|||
});
|
||||
|
||||
it('should return the queried student after he was added', async () => {
|
||||
await studentRepository.insert(new Student(username, firstName, lastName));
|
||||
await studentRepository.insert(studentRepository.create({ username, firstName, lastName }));
|
||||
|
||||
const retrievedStudent = await studentRepository.findByUsername(username);
|
||||
expect(retrievedStudent).toBeTruthy();
|
||||
|
|
|
@ -2,7 +2,6 @@ import { describe, it, expect, beforeAll } from 'vitest';
|
|||
import { TeacherRepository } from '../../../src/data/users/teacher-repository';
|
||||
import { setupTestApp } from '../../setup-tests';
|
||||
import { getTeacherRepository } from '../../../src/data/repositories';
|
||||
import { Teacher } from '../../../src/entities/users/teacher.entity';
|
||||
|
||||
const username = 'testteacher';
|
||||
const firstName = 'John';
|
||||
|
@ -30,7 +29,7 @@ describe('TeacherRepository', () => {
|
|||
});
|
||||
|
||||
it('should return the queried teacher after he was added', async () => {
|
||||
await teacherRepository.insert(new Teacher(username, firstName, lastName));
|
||||
await teacherRepository.insert(teacherRepository.create({ username, firstName, lastName }));
|
||||
|
||||
const retrievedTeacher = await teacherRepository.findByUsername(username);
|
||||
expect(retrievedTeacher).toBeTruthy();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue