Merge remote-tracking branch 'refs/remotes/origin/fix/databank-laat-toevoegen-van-meerdere-studenten-met-dezelfde-username-toe-#153' into feat/user-routes

# Conflicts:
#	backend/src/controllers/students.ts
#	backend/src/controllers/teachers.ts
#	backend/src/exceptions.ts
#	backend/src/interfaces/student.ts
#	backend/src/routes/router.ts
#	backend/src/routes/students.ts
#	backend/src/services/students.ts
#	backend/src/services/teachers.ts
#	frontend/src/controllers/controllers.ts
This commit is contained in:
Gabriellvl 2025-03-30 15:41:40 +02:00
commit 7b65d2a5b8
78 changed files with 1939 additions and 1094 deletions

View file

@ -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';
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() {
await initORM();

View file

@ -1,12 +1,7 @@
import { EnvVars, getEnvVar } from './util/envvars.js';
import { Language } from './entities/content/language.js';
// API
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
// Logging
export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info';
export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102';
export const FALLBACK_SEQ_NUM = 1;

View file

@ -37,7 +37,7 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re
return;
}
res.status(201).json({ assignment: assignment });
res.status(201).json(assignment);
}
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
@ -62,13 +62,14 @@ export async function getAssignmentHandler(req: Request<AssignmentParams>, res:
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentNumber = +req.params.id;
const full = req.query.full === 'true';
if (isNaN(assignmentNumber)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber);
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
res.json({
submissions: submissions,

View file

@ -1,5 +1,5 @@
import { Request, Response } from 'express';
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js';
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js';
import { ClassDTO } from '../interfaces/class.js';
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
@ -28,30 +28,19 @@ export async function createClassHandler(req: Request, res: Response): Promise<v
return;
}
res.status(201).json({ class: cls });
res.status(201).json(cls);
}
export async function getClassHandler(req: Request, res: Response): Promise<void> {
try {
const classId = req.params.id;
const cls = await getClass(classId);
const classId = req.params.id;
const cls = await getClass(classId);
if (!cls) {
res.status(404).json({ error: 'Class not found' });
return;
}
cls.endpoints = {
self: `${req.baseUrl}/${req.params.id}`,
invitations: `${req.baseUrl}/${req.params.id}/invitations`,
assignments: `${req.baseUrl}/${req.params.id}/assignments`,
students: `${req.baseUrl}/${req.params.id}/students`,
};
res.json(cls);
} catch (error) {
console.error('Error fetching learning objects:', error);
res.status(500).json({ error: 'Internal server error' });
if (!cls) {
res.status(404).json({ error: 'Class not found' });
return;
}
res.json(cls);
}
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
@ -67,7 +56,7 @@ export async function getClassStudentsHandler(req: Request, res: Response): Prom
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id;
const full = req.query.full === 'true'; // TODO: not implemented yet
const full = req.query.full === 'true';
const invitations = await getClassTeacherInvitations(classId, full);

View file

@ -28,6 +28,11 @@ export async function getGroupHandler(req: Request<GroupParams>, res: Response):
const group = await getGroup(classId, assignmentId, groupId, full);
if (!group) {
res.status(404).json({ error: 'Group not found' });
return;
}
res.json(group);
}
@ -66,12 +71,12 @@ export async function createGroupHandler(req: Request, res: Response): Promise<v
return;
}
res.status(201).json({ group: group });
res.status(201).json(group);
}
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid;
// Const full = req.query.full === 'true';
const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid;
@ -87,7 +92,7 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P
return;
}
const submissions = await getGroupSubmissions(classId, assignmentId, groupId);
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
res.json({
submissions: submissions,

View file

@ -4,9 +4,9 @@ import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifie
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.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) {
@ -40,7 +40,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
}
res.json(learningObjects);
res.json({ learningObjects: learningObjects });
}
export async function getLearningObject(req: Request, res: Response): Promise<void> {

View file

@ -2,13 +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, NotFoundException } from '../exceptions.js';
import { Language } from '../entities/content/language.js';
import {
PersonalizationTarget,
personalizedForGroup,
personalizedForStudent,
} from '../services/learning-paths/learning-path-personalization-util.js';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
/**
* Fetch learning paths based on query parameters.

View file

@ -48,7 +48,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi
if (!questions) {
res.status(404).json({ error: `Questions not found.` });
} else {
res.json(questions);
res.json({ questions: questions });
}
}
@ -76,12 +76,12 @@ export async function getQuestionAnswersHandler(req: Request, res: Response): Pr
return;
}
const answers = getAnswersByQuestion(questionId, full);
const answers = await getAnswersByQuestion(questionId, full);
if (!answers) {
res.status(404).json({ error: `Questions not found.` });
res.status(404).json({ error: `Questions not found` });
} else {
res.json(answers);
res.json({ answers: answers });
}
}
@ -96,7 +96,7 @@ export async function createQuestionHandler(req: Request, res: Response): Promis
const question = await createQuestion(questionDTO);
if (!question) {
res.status(400).json({ error: 'Could not add question' });
res.status(400).json({ error: 'Could not create question' });
} else {
res.json(question);
}

View file

@ -93,9 +93,10 @@ export async function getStudentGroupsHandler(req: Request, res: Response): Prom
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const full = req.query.full === 'true';
requireFields({ username });
const submissions = await getStudentSubmissions(username);
const submissions = await getStudentSubmissions(username, full);
res.json({
submissions,

View file

@ -36,10 +36,11 @@ export async function createSubmissionHandler(req: Request, res: Response) {
const submission = await createSubmission(submissionDTO);
if (!submission) {
res.status(404).json({ error: 'Submission not added' });
} else {
res.json(submission);
res.status(400).json({ error: 'Failed to create submission' });
return;
}
res.json(submission);
}
export async function deleteSubmissionHandler(req: Request, res: Response) {
@ -53,7 +54,8 @@ export async function deleteSubmissionHandler(req: Request, res: Response) {
if (!submission) {
res.status(404).json({ error: 'Submission not found' });
} else {
res.json(submission);
return;
}
res.json(submission);
}

View file

@ -1,10 +1,12 @@
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception';
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
public async save(entity: T) {
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>) {
const toDelete = await this.findOne(query);

View file

@ -2,8 +2,6 @@ import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-o
import { forkEntityManager } from '../orm.js';
import { StudentRepository } from './users/student-repository.js';
import { Student } from '../entities/users/student.entity.js';
import { User } from '../entities/users/user.entity.js';
import { UserRepository } from './users/user-repository.js';
import { Teacher } from '../entities/users/teacher.entity.js';
import { TeacherRepository } from './users/teacher-repository.js';
import { Class } from '../entities/classes/class.entity.js';

View file

@ -1,5 +1,4 @@
import { Student } from '../../entities/users/student.entity.js';
import { User } from '../../entities/users/user.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
// Import { UserRepository } from './user-repository.js';

View file

@ -1,6 +1,5 @@
import { Teacher } from '../../entities/users/teacher.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { UserRepository } from './user-repository.js';
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
public findByUsername(username: string): Promise<Teacher | null> {

View file

@ -1,4 +1,4 @@
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Class } from '../classes/class.entity.js';
import { Group } from './group.entity.js';
import { Language } from '../content/language.js';

View file

@ -1,4 +1,4 @@
import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
import { Assignment } from './assignment.entity.js';
import { Student } from '../users/student.entity.js';
import { GroupRepository } from '../../data/assignments/group-repository.js';

View file

@ -3,6 +3,12 @@ import { Student } from '../users/student.entity.js';
import { Class } from './class.entity.js';
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
export enum ClassJoinRequestStatus {
Open = 'open',
Accepted = 'accepted',
Declined = 'declined',
}
@Entity({
repository: () => ClassJoinRequestRepository,
})
@ -22,9 +28,3 @@ export class ClassJoinRequest {
@Enum(() => ClassJoinRequestStatus)
status!: ClassJoinRequestStatus;
}
export enum ClassJoinRequestStatus {
Open = 'open',
Accepted = 'accepted',
Declined = 'declined',
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -1,56 +0,0 @@
/**
* Exception for HTTP 400 Bad Request
*/
export class BadRequestException extends Error {
public status = 400;
constructor(error: string) {
super(error);
}
}
/**
* Exception for HTTP 401 Unauthorized
*/
export class UnauthorizedException extends Error {
status = 401;
constructor(message: string = 'Unauthorized') {
super(message);
}
}
/**
* Exception for HTTP 403 Forbidden
*/
export class ForbiddenException extends Error {
status = 403;
constructor(message: string = 'Forbidden') {
super(message);
}
}
/**
* Exception for HTTP 404 Not Found
*/
export class NotFoundException extends Error {
public status = 404;
constructor(error: string) {
super(error);
}
}
export class ConflictException extends Error {
public status = 409;
constructor(message: string = 'Conflict') {
super(message);
}
}
export class InternalServerError extends Error {
public status = 500;
constructor(message: string = 'Internal Server Error') {
super(message);
}
}

View 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);
}
}

View 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);
}
}

View file

@ -0,0 +1,7 @@
import { ConflictException } from './conflict-exception';
export class EntityAlreadyExistsException extends ConflictException {
constructor(message: string) {
super(message);
}
}

View 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);
}
}

View 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: string = 'Forbidden') {
super(403, message);
}
}

View 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);
}
}

View 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: string = 'Unauthorized') {
super(401, message);
}
}

View file

@ -2,7 +2,7 @@ import { FALLBACK_LANG } from '../config.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { Class } from '../entities/classes/class.entity.js';
import { languageMap } from '../entities/content/language.js';
import { GroupDTO, mapToGroupDTO } from './group.js';
import { GroupDTO } from './group.js';
export interface AssignmentDTO {
id: number;
@ -46,7 +46,5 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
assignment.within = cls;
console.log(assignment);
return assignment;
}

View file

@ -9,12 +9,6 @@ export interface ClassDTO {
teachers: string[];
students: string[];
joinRequests: string[];
endpoints?: {
self: string;
invitations: string;
assignments: string;
students: string;
};
}
export function mapToClassDTO(cls: Class): ClassDTO {

View file

@ -1,8 +1,6 @@
import { Question } from '../entities/questions/question.entity.js';
import { UserDTO } from './user.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToStudentDTO, StudentDTO } from './student.js';
import { TeacherDTO } from './teacher.js';
export interface QuestionDTO {
learningObjectIdentifier: LearningObjectIdentifier;

View file

@ -1,7 +1,8 @@
import {Student} from '../entities/users/student.entity.js';
import { Student } from '../entities/users/student.entity.js';
import { getStudentRepository } from '../data/repositories';
export interface StudentDTO {
id?: string;
id: string;
username: string;
firstName: string;
lastName: string;
@ -17,5 +18,9 @@ export function mapToStudentDTO(student: Student): StudentDTO {
}
export function mapToStudent(studentData: StudentDTO): Student {
return new Student(studentData.username, studentData.firstName, studentData.lastName);
return getStudentRepository().create({
username: studentData.username,
firstName: studentData.firstName,
lastName: studentData.lastName,
});
}

View file

@ -2,13 +2,10 @@ import { Submission } from '../entities/assignments/submission.entity.js';
import { Language } from '../entities/content/language.js';
import { GroupDTO, mapToGroupDTO } from './group.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
import { mapToUser } from './user';
import { Student } from '../entities/users/student.entity';
import { LearningObjectIdentifier } from './learning-content.js';
export interface SubmissionDTO {
learningObjectHruid: string;
learningObjectLanguage: Language;
learningObjectVersion: number;
learningObjectIdentifier: LearningObjectIdentifier;
submissionNumber?: number;
submitter: StudentDTO;
@ -17,11 +14,21 @@ export interface SubmissionDTO {
content: string;
}
export interface SubmissionDTOId {
learningObjectHruid: string;
learningObjectLanguage: Language;
learningObjectVersion: number;
submissionNumber?: number;
}
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
return {
learningObjectHruid: submission.learningObjectHruid,
learningObjectLanguage: submission.learningObjectLanguage,
learningObjectVersion: submission.learningObjectVersion,
learningObjectIdentifier: {
hruid: submission.learningObjectHruid,
language: submission.learningObjectLanguage,
version: submission.learningObjectVersion,
},
submissionNumber: submission.submissionNumber,
submitter: mapToStudentDTO(submission.submitter),
@ -31,11 +38,21 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
};
}
export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
return {
learningObjectHruid: submission.learningObjectHruid,
learningObjectLanguage: submission.learningObjectLanguage,
learningObjectVersion: submission.learningObjectVersion,
submissionNumber: submission.submissionNumber,
};
}
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
const submission = new Submission();
submission.learningObjectHruid = submissionDTO.learningObjectHruid;
submission.learningObjectLanguage = submissionDTO.learningObjectLanguage;
submission.learningObjectVersion = submissionDTO.learningObjectVersion;
submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid;
submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language;
submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!;
// Submission.submissionNumber = submissionDTO.submissionNumber;
submission.submitter = mapToStudent(submissionDTO.submitter);
// Submission.submissionTime = submissionDTO.time;

View file

@ -1,4 +1,5 @@
import { Teacher } from '../entities/users/teacher.entity.js';
import { getTeacherRepository } from '../data/repositories';
export interface TeacherDTO {
id: string;
@ -22,8 +23,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
};
}
export function mapToTeacher(TeacherData: TeacherDTO): Teacher {
const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName);
return teacher;
export function mapToTeacher(teacherData: TeacherDTO): Teacher {
return getTeacherRepository().create({
username: teacherData.username,
firstName: teacherData.firstName,
lastName: teacherData.lastName,
});
}

View file

@ -1,7 +1,7 @@
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
import LokiTransport from 'winston-loki';
import { LokiLabels } from 'loki-logger-ts';
import { LOG_LEVEL, LOKI_HOST } from '../config.js';
import { EnvVars, getEnvVar } from '../util/envvars.js';
export class Logger extends WinstonLogger {
constructor() {
@ -22,10 +22,25 @@ function initializeLogger(): Logger {
return logger;
}
const logLevel = getEnvVar(EnvVars.LogLevel);
const consoleTransport = new transports.Console({
level: getEnvVar(EnvVars.LogLevel),
format: format.combine(format.cli(), format.colorize()),
});
if (getEnvVar(EnvVars.RunMode) === 'dev') {
return createLogger({
transports: [consoleTransport],
});
}
const lokiHost = getEnvVar(EnvVars.LokiHost);
const lokiTransport: LokiTransport = new LokiTransport({
host: LOKI_HOST,
host: lokiHost,
labels: Labels,
level: LOG_LEVEL,
level: logLevel,
json: true,
format: format.combine(format.timestamp(), format.json()),
onConnectionError: (err) => {
@ -34,16 +49,11 @@ function initializeLogger(): Logger {
},
});
const consoleTransport = new transports.Console({
level: LOG_LEVEL,
format: format.combine(format.cli(), format.colorize()),
});
logger = createLogger({
transports: [lokiTransport, consoleTransport],
});
logger.debug(`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`);
logger.debug(`Logger initialized with level ${logLevel} to Loki host ${lokiHost}`);
return logger;
}

View file

@ -6,7 +6,8 @@ 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';
import { UnauthorizedException } from '../../exceptions/unauthorized-exception';
import { ForbiddenException } from '../../exceptions/forbidden-exception';
const JWKS_CACHE = true;
const JWKS_RATE_LIMIT = true;

View file

@ -0,0 +1,15 @@
import { NextFunction, Request, Response } from 'express';
import { getLogger, Logger } from '../../logging/initalize';
import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-state';
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);
}
}

View file

@ -3,7 +3,6 @@ import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js';
import { SqliteDriver } from '@mikro-orm/sqlite';
import { MikroOrmLogger } from './logging/mikroOrmLogger.js';
import { LOG_LEVEL } from './config.js';
// Import alle entity-bestanden handmatig
import { User } from './entities/users/user.entity.js';
@ -66,10 +65,12 @@ function config(testingMode: boolean = false): Options {
user: getEnvVar(EnvVars.DbUsername),
password: getEnvVar(EnvVars.DbPassword),
entities: entities,
persistOnCreate: false, // Entities should not be implicitly persisted when calling create(...), but only after
// They were saved explicitly.
// EntitiesTs: entitiesTs,
// Logging
debug: LOG_LEVEL === 'debug',
debug: getEnvVar(EnvVars.LogLevel) === 'debug',
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
};
}

View file

@ -1,11 +1,7 @@
import { Response, Router } from 'express';
import studentRouter from './students.js';
import teacherRouter from './teachers.js';
import groupRouter from './groups.js';
import assignmentRouter from './assignments.js';
import submissionRouter from './submissions.js';
import classRouter from './classes.js';
import questionRouter from './questions.js';
import authRouter from './auth.js';
import themeRoutes from './themes.js';
import learningPathRoutes from './learning-paths.js';
@ -24,11 +20,7 @@ router.get('/', (_, res: Response) => {
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */);
router.use('/group', groupRouter /* #swagger.tags = ['Group'] */);
router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */);
router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */);
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
router.use('/question', questionRouter /* #swagger.tags = ['Question'] */);
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);

View file

@ -1,7 +1,6 @@
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
const classRepository = getClassRepository();
@ -21,7 +20,7 @@ export async function getAllAssignments(classid: string, full: boolean): Promise
return assignments.map(mapToAssignmentDTOId);
}
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<Assignment | null> {
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO | null> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
@ -36,8 +35,9 @@ export async function createAssignment(classid: string, assignmentData: Assignme
const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment);
return newAssignment;
return mapToAssignmentDTO(newAssignment);
} catch (e) {
console.error(e);
return null;
}
}
@ -60,7 +60,11 @@ export async function getAssignment(classid: string, id: number): Promise<Assign
return mapToAssignmentDTO(assignment);
}
export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise<SubmissionDTO[]> {
export async function getAssignmentsSubmissions(
classid: string,
assignmentNumber: number,
full: boolean
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classid);
@ -81,5 +85,9 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe
const submissionRepository = getSubmissionRepository();
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
return submissions.map(mapToSubmissionDTO);
if (full) {
return submissions.map(mapToSubmissionDTO);
}
return submissions.map(mapToSubmissionDTOId);
}

View file

@ -1,5 +1,4 @@
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
import { Class } from '../entities/classes/class.entity.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
@ -21,16 +20,14 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
return classes.map((cls) => cls.classId!);
}
export async function createClass(classData: ClassDTO): Promise<Class | null> {
export async function createClass(classData: ClassDTO): Promise<ClassDTO | 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((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 cls = mapToClass(classData, teachers, students);
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
const classRepository = getClassRepository();
@ -42,7 +39,7 @@ export async function createClass(classData: ClassDTO): Promise<Class | null> {
});
await classRepository.save(newClass);
return newClass;
return mapToClassDTO(newClass);
} catch (e) {
logger.error(e);
return null;

View file

@ -1,4 +1,3 @@
import { GroupRepository } from '../data/assignments/group-repository.js';
import {
getAssignmentRepository,
getClassRepository,
@ -8,7 +7,7 @@ import {
} from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
const classRepository = getClassRepository();
@ -43,7 +42,7 @@ 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((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
console.log(members);
@ -103,7 +102,12 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu
return groups.map(mapToGroupDTOId);
}
export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise<SubmissionDTO[]> {
export async function getGroupSubmissions(
classId: string,
assignmentNumber: number,
groupNumber: number,
full: boolean
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const classRepository = getClassRepository();
const cls = await classRepository.findById(classId);
@ -128,5 +132,9 @@ export async function getGroupSubmissions(classId: string, assignmentNumber: num
const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForGroup(group);
return submissions.map(mapToSubmissionDTO);
if (full) {
return submissions.map(mapToSubmissionDTO);
}
return submissions.map(mapToSubmissionDTOId);
}

View file

@ -45,6 +45,13 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
return filterData(metadata, htmlUrl);
}
/**
* Generic function to fetch learning paths
*/
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
throw new Error('Function not implemented.');
}
/**
* Generic function to fetch learning objects (full data or just HRUIDs)
*/
@ -85,6 +92,3 @@ export async function getLearningObjectsFromPath(hruid: string, language: string
export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
return (await fetchLearningObjects(hruid, false, language)) as string[];
}
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
throw new Error('Function not implemented.');
}

View file

@ -103,5 +103,5 @@ export async function deleteQuestion(questionId: QuestionId) {
return null;
}
return question;
return mapToQuestionDTO(question);
}

View file

@ -1,21 +1,13 @@
import {
getClassJoinRequestRepository,
getClassRepository,
getGroupRepository,
getQuestionRepository,
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';
import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
import { getAllAssignments } from './assignments.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question';
import {ClassJoinRequestStatus} from "../entities/classes/class-join-request.entity";
import {ConflictException, NotFoundException} from "../exceptions";
import {Student} from "../entities/users/student.entity";
import {mapToStudentRequestDTO} from "../interfaces/student-request";
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
@ -47,14 +39,8 @@ export async function getStudent(username: string): Promise<StudentDTO> {
export async function createStudent(userData: StudentDTO): Promise<void> {
const studentRepository = getStudentRepository();
const user = await studentRepository.findByUsername(userData.username);
if (user) {
throw new ConflictException("Student with that sername already exists");
}
const newStudent = studentRepository.create(mapToStudent(userData));
await studentRepository.save(newStudent);
const newStudent = mapToStudent(userData);
await studentRepository.save(newStudent, { preventOverwrite: true });
}
export async function deleteStudent(username: string): Promise<void> {
@ -100,82 +86,15 @@ export async function getStudentGroups(username: string, full: boolean): Promise
return groups.map(mapToGroupDTOId);
}
export async function getStudentSubmissions(username: string): Promise<SubmissionDTO[]> {
export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const student = await fetchStudent(username);
const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForStudent(student);
return submissions.map(mapToSubmissionDTO);
}
export async function getStudentQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const student = await fetchStudent(username);
const questionRepository = getQuestionRepository();
const questions = await questionRepository.findAllByAuthor(student);
const questionsDTO = questions.map(mapToQuestionDTO);
if (full)
return questionsDTO;
return questionsDTO.map(mapToQuestionId);
}
export async function createClassJoinRequest(studentUsername: string, classId: string) {
const classRepo = getClassRepository();
const requestRepo = getClassJoinRequestRepository();
const student = await fetchStudent(studentUsername);
const cls = await classRepo.findById(classId);
if (!cls){
throw new NotFoundException("Class with id not found");
if (full) {
return submissions.map(mapToSubmissionDTO);
}
const req = await requestRepo.findByStudentAndClass(student, cls);
if (req){
throw new ConflictException("Request with student and class already exist");
}
const request = requestRepo.create({
requester: student,
class: cls,
status: ClassJoinRequestStatus.Open,
});
await requestRepo.save(request);
return submissions.map(mapToSubmissionDTOId);
}
export async function getJoinRequestsByStudent(studentUsername: string) {
const requestRepo = getClassJoinRequestRepository();
const student = await fetchStudent(studentUsername);
const requests = await requestRepo.findAllRequestsBy(student);
return requests.map(mapToStudentRequestDTO);
}
export async function deleteClassJoinRequest(studentUsername: string, classId: string) {
const requestRepo = getClassJoinRequestRepository();
const classRepo = getClassRepository();
const student = await fetchStudent(studentUsername);
const cls = await classRepo.findById(classId);
if (!cls) {
throw new NotFoundException('Class not found');
}
const request = await requestRepo.findByStudentAndClass(student, cls);
if (!request) {
throw new NotFoundException('Join request not found');
}
await requestRepo.deleteBy(student, cls);
}

View file

@ -32,7 +32,7 @@ export async function createSubmission(submissionDTO: SubmissionDTO) {
return null;
}
return submission;
return mapToSubmissionDTO(submission);
}
export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) {

View file

@ -54,14 +54,8 @@ export async function getTeacher(username: string): Promise<TeacherDTO> {
export async function createTeacher(userData: TeacherDTO): Promise<void> {
const teacherRepository: TeacherRepository = getTeacherRepository();
const user: Teacher | null = await teacherRepository.findByUsername(userData.username);
if (user){
throw new ConflictException("Teacher with that username already exists");
}
const newTeacher: Teacher = teacherRepository.create(mapToTeacher(userData));
await teacherRepository.save(newTeacher);
const newTeacher = mapToTeacher(userData);
await teacherRepository.save(newTeacher, { preventOverwrite: true });
}
export async function deleteTeacher(username: string): Promise<void> {

View file

@ -1,41 +0,0 @@
import { UserRepository } from '../data/users/user-repository.js';
import { UserDTO, mapToUser, mapToUserDTO } from '../interfaces/user.js';
import { User } from '../entities/users/user.entity.js';
export class UserService<T extends User> {
protected repository: UserRepository<T>;
constructor(repository: UserRepository<T>) {
this.repository = repository;
}
async getAllUsers(): Promise<UserDTO[]> {
const users = await this.repository.findAll();
return users.map(mapToUserDTO);
}
async getAllUserIds(): Promise<string[]> {
const users = await this.getAllUsers();
return users.map((user) => user.username);
}
async getUserByUsername(username: string): Promise<UserDTO | null> {
const user = await this.repository.findByUsername(username);
return user ? mapToUserDTO(user) : null;
}
async createUser(userData: UserDTO, UserClass: new () => T): Promise<T> {
const newUser = mapToUser(userData, new UserClass());
await this.repository.save(newUser);
return newUser;
}
async deleteUser(username: string): Promise<UserDTO | null> {
const user = await this.getUserByUsername(username);
if (!user) {
return null;
}
await this.repository.deleteByUsername(username);
return mapToUserDTO(user);
}
}

View file

@ -4,20 +4,24 @@ const IDP_PREFIX = PREFIX + 'AUTH_';
const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_';
const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_';
const CORS_PREFIX = PREFIX + 'CORS_';
const LOGGING_PREFIX = PREFIX + 'LOGGING_';
type EnvVar = { key: string; required?: boolean; defaultValue?: any };
export const EnvVars: { [key: string]: EnvVar } = {
Port: { key: PREFIX + 'PORT', defaultValue: 3000 },
LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' },
FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' },
RunMode: { key: PREFIX + 'RUN_MODE', defaultValue: 'dev' },
DbHost: { key: DB_PREFIX + 'HOST', required: true },
DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 },
DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' },
DbUsername: { key: DB_PREFIX + 'USERNAME', required: true },
DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true },
DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false },
LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' },
FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' },
UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' },
IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true },
IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true },
IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true },
@ -25,8 +29,12 @@ export const EnvVars: { [key: string]: EnvVar } = {
IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true },
IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true },
IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' },
CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' },
CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' },
LogLevel: { key: LOGGING_PREFIX + 'LEVEL', defaultValue: 'info' },
LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' },
} as const;
/**

View file

@ -8,12 +8,12 @@ const logger: Logger = getLogger();
export function loadTranslations<T>(language: string): T {
try {
const filePath = path.join(process.cwd(), '_i18n', `${language}.yml`);
const filePath = path.join(process.cwd(), 'i18n', `${language}.yml`);
const yamlFile = fs.readFileSync(filePath, 'utf8');
return yaml.load(yamlFile) as T;
} catch (error) {
logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error);
const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`);
const fallbackPath = path.join(process.cwd(), 'i18n', `${FALLBACK_LANG}.yml`);
return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T;
}
}