diff --git a/.github/workflows/backend-testing.yml b/.github/workflows/backend-testing.yml new file mode 100644 index 00000000..0d0b1f3f --- /dev/null +++ b/.github/workflows/backend-testing.yml @@ -0,0 +1,45 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, run backend tests across different versions of node (here 22.x) +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Backend Testing + +# Workflow runs when: +# - a backend js/ts file on "dev" changes +# - a non-draft PR to "dev" with backend js/ts files is opened, is reopened, or changes +# - a draft PR to "dev" with backend js/ts files is marked as ready for review +on: + push: + branches: [ "dev" ] + paths: + - 'backend/src/**.[jt]s' + - 'backend/tests/**.[jt]s' + - 'backend/vitest.config.ts' + pull_request: + branches: [ "dev" ] + types: ["synchronize", "ready_for_review", "opened", "reopened"] + paths: + - 'backend/src/**.[jt]s' + - 'backend/tests/**.[jt]s' + - 'backend/vitest.config.ts' + + +jobs: + test: + name: Run backend unit tests + if: '! github.event.pull_request.draft' + runs-on: [self-hosted, Linux, X64] + + strategy: + matrix: + node-version: [22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run test:unit -w backend diff --git a/.github/workflows/frontend-testing.yml b/.github/workflows/frontend-testing.yml new file mode 100644 index 00000000..ff7bde4d --- /dev/null +++ b/.github/workflows/frontend-testing.yml @@ -0,0 +1,54 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, run frontend tests across different versions of node (here 22.x) +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Frontend Testing + +# Workflow runs when: +# - a frontend js/ts/vue/css file on "dev" changes +# - a non-draft PR to "dev" with frontend js/ts/vue/css files is opened, is reopened, or changes +# - a draft PR to "dev" with frontend js/ts/vue/css files is marked as ready for review +on: + push: + branches: [ "dev" ] + paths: + - 'frontend/src/**.[jt]s' + - 'frontend/src/**.vue' + - 'frontend/src/**.css' + - 'frontend/tests/**.[jt]s' + - 'frontend/tests/**.vue' + - 'frontend/tests/**.css' + - 'frontend/vitest.config.ts' + - 'frontend/playwright.config.ts' + pull_request: + branches: [ "dev" ] + types: ["synchronize", "ready_for_review", "opened", "reopened"] + paths: + - 'frontend/src/**.[jt]s' + - 'frontend/src/**.vue' + - 'frontend/src/**.css' + - 'frontend/tests/**.[jt]s' + - 'frontend/tests/**.vue' + - 'frontend/tests/**.css' + - 'frontend/vitest.config.ts' + - 'frontend/playwright.config.ts' + +jobs: + test: + name: Run frontend unit tests + if: '! github.event.pull_request.draft' + runs-on: [self-hosted, Linux, X64] + + strategy: + matrix: + node-version: [22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run test:unit -w frontend diff --git a/backend/eslint.config.ts b/backend/eslint.config.ts index 6b696021..f5f225b2 100644 --- a/backend/eslint.config.ts +++ b/backend/eslint.config.ts @@ -8,14 +8,4 @@ export default [ globals: globals.node, }, }, - - { - files: ['tests/**/*.ts'], - languageOptions: { - globals: globals.node, - }, - rules: { - 'no-console': 'off', - }, - }, ]; diff --git a/backend/package.json b/backend/package.json index 4e3b890d..c08fb1dc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,7 @@ "format": "prettier --write src/", "format-check": "prettier --check src/", "lint": "eslint . --fix", - "test:unit": "vitest" + "test:unit": "vitest --run" }, "dependencies": { "@mikro-orm/core": "6.4.9", diff --git a/backend/src/app.ts b/backend/src/app.ts index 0c5e8892..cf10a6df 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,15 +5,16 @@ import cors from './middleware/cors.js'; import { getLogger, Logger } from './logging/initalize.js'; import { responseTimeLogger } from './logging/responseTimeLogger.js'; import responseTime from 'response-time'; -import { EnvVars, getNumericEnvVar } from './util/envvars.js'; +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(); const app: Express = express(); -const port: string | number = getNumericEnvVar(EnvVars.Port); +const port: string | number = getNumericEnvVar(envVars.Port); app.use(express.json()); app.use(cors); @@ -26,7 +27,9 @@ app.use('/api', apiRouter); // Swagger app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); -async function startServer() { +app.use(errorHandler); + +async function startServer(): Promise { await initORM(); app.listen(port, () => { diff --git a/backend/src/config.ts b/backend/src/config.ts index b9974a3b..9b4702b5 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -1,7 +1,7 @@ -import { EnvVars, getEnvVar } from './util/envvars.js'; +import { envVars, getEnvVar } from './util/envVars.js'; // API -export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); -export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); +export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl); +export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage); export const FALLBACK_SEQ_NUM = 1; diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index 03332469..c8dd1ec8 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; import { AssignmentDTO } from '../interfaces/assignment.js'; -// Typescript is annoy with with parameter forwarding from class.ts +// Typescript is annoying with parameter forwarding from class.ts interface AssignmentParams { classid: string; id: string; @@ -37,11 +37,11 @@ export async function createAssignmentHandler(req: Request, re return; } - res.status(201).json({ assignment: assignment }); + res.status(201).json(assignment); } export async function getAssignmentHandler(req: Request, res: Response): Promise { - const id = +req.params.id; + const id = Number(req.params.id); const classid = req.params.classid; if (isNaN(id)) { @@ -61,14 +61,15 @@ export async function getAssignmentHandler(req: Request, res: export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise { const classid = req.params.classid; - const assignmentNumber = +req.params.id; + const assignmentNumber = Number(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, diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index 409ead0c..b87eaf7b 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -1,16 +1,16 @@ -import { EnvVars, getEnvVar } from '../util/envvars.js'; +import { envVars, getEnvVar } from '../util/envVars.js'; -type FrontendIdpConfig = { +interface FrontendIdpConfig { authority: string; clientId: string; scope: string; responseType: string; -}; +} -type FrontendAuthConfig = { +interface FrontendAuthConfig { student: FrontendIdpConfig; teacher: FrontendIdpConfig; -}; +} const SCOPE = 'openid profile email'; const RESPONSE_TYPE = 'code'; @@ -18,14 +18,14 @@ const RESPONSE_TYPE = 'code'; export function getFrontendAuthConfig(): FrontendAuthConfig { return { student: { - authority: getEnvVar(EnvVars.IdpStudentUrl), - clientId: getEnvVar(EnvVars.IdpStudentClientId), + authority: getEnvVar(envVars.IdpStudentUrl), + clientId: getEnvVar(envVars.IdpStudentClientId), scope: SCOPE, responseType: RESPONSE_TYPE, }, teacher: { - authority: getEnvVar(EnvVars.IdpTeacherUrl), - clientId: getEnvVar(EnvVars.IdpTeacherClientId), + authority: getEnvVar(envVars.IdpTeacherUrl), + clientId: getEnvVar(envVars.IdpTeacherClientId), scope: SCOPE, responseType: RESPONSE_TYPE, }, diff --git a/backend/src/controllers/classes.ts b/backend/src/controllers/classes.ts index ca2f5698..7526f7c4 100644 --- a/backend/src/controllers/classes.ts +++ b/backend/src/controllers/classes.ts @@ -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 { @@ -28,30 +28,19 @@ export async function createClassHandler(req: Request, res: Response): Promise { - 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 { @@ -67,7 +56,7 @@ export async function getClassStudentsHandler(req: Request, res: Response): Prom export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise { 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); diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index b7bfd212..7de3e114 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -12,14 +12,14 @@ interface GroupParams { export async function getGroupHandler(req: Request, res: Response): Promise { const classId = req.params.classid; const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); return; } - const groupId = +req.params.groupid!; // Can't be undefined + const groupId = Number(req.params.groupid!); // Can't be undefined if (isNaN(groupId)) { res.status(400).json({ error: 'Group id must be a number' }); @@ -28,6 +28,11 @@ export async function getGroupHandler(req: Request, res: Response): const group = await getGroup(classId, assignmentId, groupId, full); + if (!group) { + res.status(404).json({ error: 'Group not found' }); + return; + } + res.json(group); } @@ -35,7 +40,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< const classId = req.params.classid; const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); @@ -51,7 +56,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< export async function createGroupHandler(req: Request, res: Response): Promise { const classid = req.params.classid; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); @@ -66,28 +71,28 @@ export async function createGroupHandler(req: Request, res: Response): Promise { const classId = req.params.classid; - // Const full = req.query.full === 'true'; + const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); return; } - const groupId = +req.params.groupid!; // Can't be undefined + const groupId = Number(req.params.groupid); // Can't be undefined if (isNaN(groupId)) { res.status(400).json({ error: 'Group id must be a number' }); return; } - const submissions = await getGroupSubmissions(classId, assignmentId, groupId); + const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full); res.json({ submissions: submissions, diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 455a4006..fc79ef0d 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -2,19 +2,19 @@ import { Request, Response } from 'express'; import { FALLBACK_LANG } from '../config.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 { 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) { throw new BadRequestException('HRUID is required.'); } return { - hruid: req.params.hruid as string, - language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, + hruid: req.params.hruid, + language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language, version: parseInt(req.query.version as string), }; } @@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif throw new BadRequestException('HRUID is required.'); } return { - hruid: req.params.hruid as string, + hruid: req.params.hruid, language: (req.query.language as Language) || FALLBACK_LANG, }; } @@ -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 { diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 37f92d91..04e44b59 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -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. diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 917b48ae..6735c305 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu return { hruid, language: (lang as Language) || FALLBACK_LANG, - version: +version, + version: Number(version), }; } @@ -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); } diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts index 6c253cff..7e0b5565 100644 --- a/backend/src/controllers/students.ts +++ b/backend/src/controllers/students.ts @@ -9,29 +9,21 @@ import { getStudentGroups, getStudentSubmissions, } from '../services/students.js'; -import { ClassDTO } from '../interfaces/class.js'; -import { getAllAssignments } from '../services/assignments.js'; -import { getUserHandler } from './users.js'; -import { Student } from '../entities/users/student.entity.js'; import { StudentDTO } from '../interfaces/student.js'; -import { getStudentRepository } from '../data/repositories.js'; -import { UserDTO } from '../interfaces/user.js'; // TODO: accept arguments (full, ...) // TODO: endpoints export async function getAllStudentsHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const studentRepository = getStudentRepository(); - - const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents(); + const students = await getAllStudents(full); if (!students) { res.status(404).json({ error: `Student not found.` }); return; } - res.status(201).json(students); + res.json({ students: students }); } export async function getStudentHandler(req: Request, res: Response): Promise { @@ -51,10 +43,10 @@ export async function getStudentHandler(req: Request, res: Response): Promise { const userData = req.body as StudentDTO; if (!userData.username || !userData.firstName || !userData.lastName) { @@ -65,10 +57,18 @@ export async function createStudentHandler(req: Request, res: Response) { } const newUser = await createStudent(userData); + + if (!newUser) { + res.status(500).json({ + error: 'Something went wrong while creating student', + }); + return; + } + res.status(201).json(newUser); } -export async function deleteStudentHandler(req: Request, res: Response) { +export async function deleteStudentHandler(req: Request, res: Response): Promise { const username = req.params.username; if (!username) { @@ -88,25 +88,12 @@ export async function deleteStudentHandler(req: Request, res: Response) { } export async function getStudentClassesHandler(req: Request, res: Response): Promise { - try { - const full = req.query.full === 'true'; - const username = req.params.id; + const full = req.query.full === 'true'; + const username = req.params.id; - const classes = await getStudentClasses(username, full); + const classes = await getStudentClasses(username, full); - res.json({ - classes: classes, - endpoints: { - self: `${req.baseUrl}/${req.params.id}`, - classes: `${req.baseUrl}/${req.params.id}/invitations`, - questions: `${req.baseUrl}/${req.params.id}/assignments`, - students: `${req.baseUrl}/${req.params.id}/students`, - }, - }); - } catch (error) { - console.error('Error fetching learning objects:', error); - res.status(500).json({ error: 'Internal server error' }); - } + res.json({ classes: classes }); } // TODO @@ -137,8 +124,9 @@ export async function getStudentGroupsHandler(req: Request, res: Response): Prom export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise { const username = req.params.id; + const full = req.query.full === 'true'; - const submissions = await getStudentSubmissions(username); + const submissions = await getStudentSubmissions(username, full); res.json({ submissions: submissions, diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 1e66dbe9..67c1d3a9 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -10,7 +10,7 @@ interface SubmissionParams { export async function getSubmissionHandler(req: Request, res: Response): Promise { const lohruid = req.params.hruid; - const submissionNumber = +req.params.id; + const submissionNumber = Number(req.params.id); if (isNaN(submissionNumber)) { res.status(400).json({ error: 'Submission number is not a number' }); @@ -30,21 +30,22 @@ export async function getSubmissionHandler(req: Request, res: res.json(submission); } -export async function createSubmissionHandler(req: Request, res: Response) { +export async function createSubmissionHandler(req: Request, res: Response): Promise { const submissionDTO = req.body as SubmissionDTO; 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) { +export async function deleteSubmissionHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; - const submissionNumber = +req.params.id; + const submissionNumber = Number(req.params.id); const lang = languageMap[req.query.language as string] || Language.Dutch; const version = (req.query.version || 1) as number; @@ -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); } diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts index 52e5e713..06316681 100644 --- a/backend/src/controllers/teachers.ts +++ b/backend/src/controllers/teachers.ts @@ -4,33 +4,23 @@ import { deleteTeacher, getAllTeachers, getClassesByTeacher, - getClassIdsByTeacher, - getQuestionIdsByTeacher, getQuestionsByTeacher, - getStudentIdsByTeacher, getStudentsByTeacher, getTeacher, } from '../services/teachers.js'; -import { ClassDTO } from '../interfaces/class.js'; -import { StudentDTO } from '../interfaces/student.js'; -import { QuestionDTO, QuestionId } from '../interfaces/question.js'; -import { Teacher } from '../entities/users/teacher.entity.js'; import { TeacherDTO } from '../interfaces/teacher.js'; -import { getTeacherRepository } from '../data/repositories.js'; export async function getAllTeachersHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const teacherRepository = getTeacherRepository(); - - const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers(); + const teachers = await getAllTeachers(full); if (!teachers) { res.status(404).json({ error: `Teacher not found.` }); return; } - res.status(201).json(teachers); + res.json({ teachers: teachers }); } export async function getTeacherHandler(req: Request, res: Response): Promise { @@ -45,15 +35,15 @@ export async function getTeacherHandler(req: Request, res: Response): Promise { const userData = req.body as TeacherDTO; if (!userData.username || !userData.firstName || !userData.lastName) { @@ -64,10 +54,16 @@ export async function createTeacherHandler(req: Request, res: Response) { } const newUser = await createTeacher(userData); + + if (!newUser) { + res.status(400).json({ error: 'Failed to create teacher' }); + return; + } + res.status(201).json(newUser); } -export async function deleteTeacherHandler(req: Request, res: Response) { +export async function deleteTeacherHandler(req: Request, res: Response): Promise { const username = req.params.username; if (!username) { @@ -78,7 +74,7 @@ export async function deleteTeacherHandler(req: Request, res: Response) { const deletedUser = await deleteTeacher(username); if (!deletedUser) { res.status(404).json({ - error: `User with username '${username}' not found.`, + error: `User '${username}' not found.`, }); return; } @@ -87,58 +83,58 @@ export async function deleteTeacherHandler(req: Request, res: Response) { } export async function getTeacherClassHandler(req: Request, res: Response): Promise { - try { - const username = req.params.username as string; - const full = req.query.full === 'true'; + const username = req.params.username; + const full = req.query.full === 'true'; - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username); - - res.status(201).json(classes); - } catch (error) { - console.error('Error fetching classes by teacher:', error); - res.status(500).json({ error: 'Internal server error' }); + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; } + + const classes = await getClassesByTeacher(username, full); + + if (!classes) { + res.status(404).json({ error: 'Teacher not found' }); + return; + } + + res.json({ classes: classes }); } export async function getTeacherStudentHandler(req: Request, res: Response): Promise { - try { - const username = req.params.username as string; - const full = req.query.full === 'true'; + const username = req.params.username; + const full = req.query.full === 'true'; - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username); - - res.status(201).json(students); - } catch (error) { - console.error('Error fetching students by teacher:', error); - res.status(500).json({ error: 'Internal server error' }); + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; } + + const students = await getStudentsByTeacher(username, full); + + if (!students) { + res.status(404).json({ error: 'Teacher not found' }); + return; + } + + res.json({ students: students }); } export async function getTeacherQuestionHandler(req: Request, res: Response): Promise { - try { - const username = req.params.username as string; - const full = req.query.full === 'true'; + const username = req.params.username; + const full = req.query.full === 'true'; - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username); - - res.status(201).json(questions); - } catch (error) { - console.error('Error fetching questions by teacher:', error); - res.status(500).json({ error: 'Internal server error' }); + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; } + + const questions = await getQuestionsByTeacher(username, full); + + if (!questions) { + res.status(404).json({ error: 'Teacher not found' }); + return; + } + + res.json({ questions: questions }); } diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index 8406cf5f..b34d0c80 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -3,25 +3,23 @@ import { themes } from '../data/themes.js'; import { loadTranslations } from '../util/translation-helper.js'; interface Translations { - curricula_page: { - [key: string]: { title: string; description?: string }; - }; + curricula_page: Record; } -export function getThemesHandler(req: Request, res: Response) { - const language = (req.query.language as string)?.toLowerCase() || 'nl'; +export function getThemesHandler(req: Request, res: Response): void { + const language = ((req.query.language as string) || 'nl').toLowerCase(); const translations = loadTranslations(language); const themeList = themes.map((theme) => ({ key: theme.title, - title: translations.curricula_page[theme.title]?.title || theme.title, - description: translations.curricula_page[theme.title]?.description, + title: translations.curricula_page[theme.title].title || theme.title, + description: translations.curricula_page[theme.title].description, image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, })); res.json(themeList); } -export function getHruidsByThemeHandler(req: Request, res: Response) { +export function getHruidsByThemeHandler(req: Request, res: Response): void { const themeKey = req.params.theme; if (!themeKey) { diff --git a/backend/src/controllers/users.ts b/backend/src/controllers/users.ts deleted file mode 100644 index 850c6549..00000000 --- a/backend/src/controllers/users.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Request, Response } from 'express'; -import { UserService } from '../services/users.js'; -import { UserDTO } from '../interfaces/user.js'; -import { User } from '../entities/users/user.entity.js'; - -export async function getAllUsersHandler(req: Request, res: Response, service: UserService): Promise { - try { - const full = req.query.full === 'true'; - - const users: UserDTO[] | string[] = full ? await service.getAllUsers() : await service.getAllUserIds(); - - if (!users) { - res.status(404).json({ error: `Users not found.` }); - return; - } - - res.status(201).json(users); - } catch (error) { - console.error('❌ Error fetching users:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} - -export async function getUserHandler(req: Request, res: Response, service: UserService): Promise { - try { - const username = req.params.username as string; - - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const user = await service.getUserByUsername(username); - - if (!user) { - res.status(404).json({ - error: `User with username '${username}' not found.`, - }); - return; - } - - res.status(201).json(user); - } catch (error) { - console.error('❌ Error fetching users:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} - -export async function createUserHandler(req: Request, res: Response, service: UserService, UserClass: new () => T) { - try { - console.log('req', req); - const userData = req.body as UserDTO; - - if (!userData.username || !userData.firstName || !userData.lastName) { - res.status(400).json({ - error: 'Missing required fields: username, firstName, lastName', - }); - return; - } - - const newUser = await service.createUser(userData, UserClass); - res.status(201).json(newUser); - } catch (error) { - console.error('❌ Error creating user:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} - -export async function deleteUserHandler(req: Request, res: Response, service: UserService) { - try { - const username = req.params.username; - - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const deletedUser = await service.deleteUser(username); - if (!deletedUser) { - res.status(404).json({ - error: `User with username '${username}' not found.`, - }); - return; - } - - res.status(200).json(deletedUser); - } catch (error) { - console.error('❌ Error deleting user:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} diff --git a/backend/src/data/assignments/assignment-repository.ts b/backend/src/data/assignments/assignment-repository.ts index c3c457d3..3de5031d 100644 --- a/backend/src/data/assignments/assignment-repository.ts +++ b/backend/src/data/assignments/assignment-repository.ts @@ -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 { - public findByClassAndId(within: Class, id: number): Promise { + public async findByClassAndId(within: Class, id: number): Promise { return this.findOne({ within: within, id: id }); } - public findAllAssignmentsInClass(within: Class): Promise { + public async findAllAssignmentsInClass(within: Class): Promise { return this.findAll({ where: { within: within } }); } - public deleteByClassAndId(within: Class, id: number): Promise { + public async deleteByClassAndId(within: Class, id: number): Promise { return this.deleteWhere({ within: within, id: id }); } } diff --git a/backend/src/data/assignments/group-repository.ts b/backend/src/data/assignments/group-repository.ts index eb1b09e2..f06080f7 100644 --- a/backend/src/data/assignments/group-repository.ts +++ b/backend/src/data/assignments/group-repository.ts @@ -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 { - public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { + public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { return this.findOne( { assignment: assignment, @@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository { { populate: ['members'] } ); } - public findAllGroupsForAssignment(assignment: Assignment): Promise { + public async findAllGroupsForAssignment(assignment: Assignment): Promise { return this.findAll({ where: { assignment: assignment }, populate: ['members'], }); } - public findAllGroupsWithStudent(student: Student): Promise { + public async findAllGroupsWithStudent(student: Student): Promise { return this.find({ members: student }, { populate: ['members'] }); } - public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { + public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { return this.deleteWhere({ assignment: assignment, groupNumber: groupNumber, diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index 251823fa..f5090adc 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object import { Student } from '../../entities/users/student.entity.js'; export class SubmissionRepository extends DwengoEntityRepository { - public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { + public async findSubmissionByLearningObjectAndSubmissionNumber( + loId: LearningObjectIdentifier, + submissionNumber: number + ): Promise { return this.findOne({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, @@ -14,7 +17,7 @@ export class SubmissionRepository extends DwengoEntityRepository { }); } - public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise { + public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise { return this.findOne( { learningObjectHruid: loId.hruid, @@ -26,7 +29,7 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } - public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise { + public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise { return this.findOne( { learningObjectHruid: loId.hruid, @@ -38,15 +41,15 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } - public findAllSubmissionsForGroup(group: Group): Promise { + public async findAllSubmissionsForGroup(group: Group): Promise { return this.find({ onBehalfOf: group }); } - public findAllSubmissionsForStudent(student: Student): Promise { + public async findAllSubmissionsForStudent(student: Student): Promise { return this.find({ submitter: student }); } - public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { + public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { return this.deleteWhere({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, diff --git a/backend/src/data/classes/class-join-request-repository.ts b/backend/src/data/classes/class-join-request-repository.ts index c1443c1c..1cd0288c 100644 --- a/backend/src/data/classes/class-join-request-repository.ts +++ b/backend/src/data/classes/class-join-request-repository.ts @@ -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 { - public findAllRequestsBy(requester: Student): Promise { + public async findAllRequestsBy(requester: Student): Promise { return this.findAll({ where: { requester: requester } }); } - public findAllOpenRequestsTo(clazz: Class): Promise { + public async findAllOpenRequestsTo(clazz: Class): Promise { return this.findAll({ where: { class: clazz } }); } - public deleteBy(requester: Student, clazz: Class): Promise { + public async deleteBy(requester: Student, clazz: Class): Promise { return this.deleteWhere({ requester: requester, class: clazz }); } } diff --git a/backend/src/data/classes/class-repository.ts b/backend/src/data/classes/class-repository.ts index 0ceed98e..f4e0723f 100644 --- a/backend/src/data/classes/class-repository.ts +++ b/backend/src/data/classes/class-repository.ts @@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js'; import { Teacher } from '../../entities/users/teacher.entity'; export class ClassRepository extends DwengoEntityRepository { - public findById(id: string): Promise { + public async findById(id: string): Promise { return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); } - public deleteById(id: string): Promise { + public async deleteById(id: string): Promise { return this.deleteWhere({ classId: id }); } - public findByStudent(student: Student): Promise { + public async findByStudent(student: Student): Promise { return this.find( { students: student }, { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe ); } - public findByTeacher(teacher: Teacher): Promise { + public async findByTeacher(teacher: Teacher): Promise { return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); } } diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 6b94deec..ce059ca8 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -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 { - public findAllInvitationsForClass(clazz: Class): Promise { + public async findAllInvitationsForClass(clazz: Class): Promise { return this.findAll({ where: { class: clazz } }); } - public findAllInvitationsBy(sender: Teacher): Promise { + public async findAllInvitationsBy(sender: Teacher): Promise { return this.findAll({ where: { sender: sender } }); } - public findAllInvitationsFor(receiver: Teacher): Promise { + public async findAllInvitationsFor(receiver: Teacher): Promise { return this.findAll({ where: { receiver: receiver } }); } - public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise { + public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise { return this.deleteWhere({ sender: sender, receiver: receiver, diff --git a/backend/src/data/content/attachment-repository.ts b/backend/src/data/content/attachment-repository.ts index 95c5ab1c..73baa943 100644 --- a/backend/src/data/content/attachment-repository.ts +++ b/backend/src/data/content/attachment-repository.ts @@ -4,7 +4,7 @@ import { Language } from '../../entities/content/language'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; export class AttachmentRepository extends DwengoEntityRepository { - public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise { + public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise { return this.findOne({ learningObject: { hruid: learningObjectId.hruid, @@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository { }); } - public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise { + public async findByMostRecentVersionOfLearningObjectAndName( + hruid: string, + language: Language, + attachmentName: string + ): Promise { return this.findOne( { learningObject: { diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 49b4c536..4684c6cc 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -5,7 +5,7 @@ import { Language } from '../../entities/content/language.js'; import { Teacher } from '../../entities/users/teacher.entity.js'; export class LearningObjectRepository extends DwengoEntityRepository { - public findByIdentifier(identifier: LearningObjectIdentifier): Promise { + public async findByIdentifier(identifier: LearningObjectIdentifier): Promise { return this.findOne( { hruid: identifier.hruid, @@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository { return this.findOne( { hruid: hruid, @@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository { + public async findAllByTeacher(teacher: Teacher): Promise { return this.find( { admins: teacher }, { populate: ['admins'] } // Make sure to load admin relations diff --git a/backend/src/data/content/learning-path-repository.ts b/backend/src/data/content/learning-path-repository.ts index a2f9b47e..e34508ec 100644 --- a/backend/src/data/content/learning-path-repository.ts +++ b/backend/src/data/content/learning-path-repository.ts @@ -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 { - public findByHruidAndLanguage(hruid: string, language: Language): Promise { + public async findByHruidAndLanguage(hruid: string, language: Language): Promise { return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); } diff --git a/backend/src/data/dwengo-entity-repository.ts b/backend/src/data/dwengo-entity-repository.ts index 6538d6f5..1267c726 100644 --- a/backend/src/data/dwengo-entity-repository.ts +++ b/backend/src/data/dwengo-entity-repository.ts @@ -1,12 +1,14 @@ import { EntityRepository, FilterQuery } from '@mikro-orm/core'; +import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js'; export abstract class DwengoEntityRepository extends EntityRepository { - public async save(entity: T) { - const em = this.getEntityManager(); - em.persist(entity); - await em.flush(); + public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise { + 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) { + public async deleteWhere(query: FilterQuery): Promise { const toDelete = await this.findOne(query); const em = this.getEntityManager(); if (toDelete) { diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index a28342bd..a50bfd28 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -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 { - public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise { + public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise { const answerEntity = this.create({ toQuestion: answer.toQuestion, author: answer.author, @@ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository { }); return this.insert(answerEntity); } - public findAllAnswersToQuestion(question: Question): Promise { + public async findAllAnswersToQuestion(question: Question): Promise { return this.findAll({ where: { toQuestion: question }, orderBy: { sequenceNumber: 'ASC' }, }); } - public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { + public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { return this.deleteWhere({ toQuestion: question, sequenceNumber: sequenceNumber, diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 9207e1dd..596b562c 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -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 { - public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { + public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { const questionEntity = this.create({ learningObjectHruid: question.loId.hruid, learningObjectLanguage: question.loId.language, @@ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository { questionEntity.content = question.content; return this.insert(questionEntity); } - public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise { + public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise { return this.findAll({ where: { learningObjectHruid: loId.hruid, @@ -33,7 +33,7 @@ export class QuestionRepository extends DwengoEntityRepository { }, }); } - public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise { + public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise { return this.deleteWhere({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, diff --git a/backend/src/data/repositories.ts b/backend/src/data/repositories.ts index 02385109..f09c3c75 100644 --- a/backend/src/data/repositories.ts +++ b/backend/src/data/repositories.ts @@ -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'; @@ -36,8 +34,8 @@ let entityManager: EntityManager | undefined; /** * Execute all the database operations within the function f in a single transaction. */ -export function transactional(f: () => Promise) { - entityManager?.transactional(f); +export async function transactional(f: () => Promise): Promise { + await entityManager?.transactional(f); } function repositoryGetter>(entity: EntityName): () => R { diff --git a/backend/src/data/users/student-repository.ts b/backend/src/data/users/student-repository.ts index 0792678d..2efca048 100644 --- a/backend/src/data/users/student-repository.ts +++ b/backend/src/data/users/student-repository.ts @@ -1,15 +1,11 @@ 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'; - -// Export class StudentRepository extends UserRepository {} export class StudentRepository extends DwengoEntityRepository { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username: username }); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username: username }); } } diff --git a/backend/src/data/users/teacher-repository.ts b/backend/src/data/users/teacher-repository.ts index 2b2bee75..aa915627 100644 --- a/backend/src/data/users/teacher-repository.ts +++ b/backend/src/data/users/teacher-repository.ts @@ -1,12 +1,11 @@ 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 { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username: username }); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username: username }); } } diff --git a/backend/src/data/users/user-repository.ts b/backend/src/data/users/user-repository.ts index 21497b79..44eb0bc7 100644 --- a/backend/src/data/users/user-repository.ts +++ b/backend/src/data/users/user-repository.ts @@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { User } from '../../entities/users/user.entity.js'; export class UserRepository extends DwengoEntityRepository { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username } as Partial); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username } as Partial); } } diff --git a/backend/src/entities/assignments/assignment.entity.ts b/backend/src/entities/assignments/assignment.entity.ts index 692e2112..daa71ed6 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -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'; diff --git a/backend/src/entities/assignments/group.entity.ts b/backend/src/entities/assignments/group.entity.ts index 213e0f38..cfe21f7f 100644 --- a/backend/src/entities/assignments/group.entity.ts +++ b/backend/src/entities/assignments/group.entity.ts @@ -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'; diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index fbaa2791..e4330e0d 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -16,7 +16,7 @@ export class Submission { learningObjectLanguage!: Language; @PrimaryKey({ type: 'numeric' }) - learningObjectVersion: number = 1; + learningObjectVersion = 1; @PrimaryKey({ type: 'integer', autoincrement: true }) submissionNumber?: number; diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index bdef1f52..fdf13aa9 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -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', -} diff --git a/backend/src/entities/content/educational-goal.entity.ts b/backend/src/entities/content/educational-goal.entity.ts new file mode 100644 index 00000000..fafe1a01 --- /dev/null +++ b/backend/src/entities/content/educational-goal.entity.ts @@ -0,0 +1,10 @@ +import { Embeddable, Property } from '@mikro-orm/core'; + +@Embeddable() +export class EducationalGoal { + @Property({ type: 'string' }) + source!: string; + + @Property({ type: 'string' }) + id!: string; +} diff --git a/backend/src/entities/content/learning-object-identifier.ts b/backend/src/entities/content/learning-object-identifier.ts index 3c020bd7..9234afa7 100644 --- a/backend/src/entities/content/learning-object-identifier.ts +++ b/backend/src/entities/content/learning-object-identifier.ts @@ -5,5 +5,7 @@ export class LearningObjectIdentifier { public hruid: string, public language: Language, public version: number - ) {} + ) { + // Do nothing + } } diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index 9eda22ba..e352a10a 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -1,28 +1,12 @@ -import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; +import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Language } from './language.js'; import { Attachment } from './attachment.entity.js'; import { Teacher } from '../users/teacher.entity.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; import { v4 } from 'uuid'; import { LearningObjectRepository } from '../../data/content/learning-object-repository.js'; - -@Embeddable() -export class EducationalGoal { - @Property({ type: 'string' }) - source!: string; - - @Property({ type: 'string' }) - id!: string; -} - -@Embeddable() -export class ReturnValue { - @Property({ type: 'string' }) - callbackUrl!: string; - - @Property({ type: 'json' }) - callbackSchema!: string; -} +import { EducationalGoal } from './educational-goal.entity.js'; +import { ReturnValue } from './return-value.entity.js'; @Entity({ repository: () => LearningObjectRepository }) export class LearningObject { @@ -36,7 +20,7 @@ export class LearningObject { language!: Language; @PrimaryKey({ type: 'number' }) - version: number = 1; + version = 1; @Property({ type: 'uuid', unique: true }) uuid = v4(); @@ -62,7 +46,7 @@ export class LearningObject { targetAges?: number[] = []; @Property({ type: 'bool' }) - teacherExclusive: boolean = false; + teacherExclusive = false; @Property({ type: 'array' }) skosConcepts: string[] = []; @@ -74,10 +58,10 @@ export class LearningObject { educationalGoals: EducationalGoal[] = []; @Property({ type: 'string' }) - copyright: string = ''; + copyright = ''; @Property({ type: 'string' }) - license: string = ''; + license = ''; @Property({ type: 'smallint', nullable: true }) difficulty?: number; @@ -91,7 +75,7 @@ export class LearningObject { returnValue!: ReturnValue; @Property({ type: 'bool' }) - available: boolean = true; + available = true; @Property({ type: 'string', nullable: true }) contentLocation?: string; diff --git a/backend/src/entities/content/return-value.entity.ts b/backend/src/entities/content/return-value.entity.ts new file mode 100644 index 00000000..d38b0693 --- /dev/null +++ b/backend/src/entities/content/return-value.entity.ts @@ -0,0 +1,10 @@ +import { Embeddable, Property } from '@mikro-orm/core'; + +@Embeddable() +export class ReturnValue { + @Property({ type: 'string' }) + callbackUrl!: string; + + @Property({ type: 'json' }) + callbackSchema!: string; +} diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 058ba6b3..09e3cd46 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -15,7 +15,7 @@ export class Question { learningObjectLanguage!: Language; @PrimaryKey({ type: 'number' }) - learningObjectVersion: number = 1; + learningObjectVersion = 1; @PrimaryKey({ type: 'integer', autoincrement: true }) sequenceNumber?: number; diff --git a/backend/src/entities/users/student.entity.ts b/backend/src/entities/users/student.entity.ts index da5b4367..58e82765 100644 --- a/backend/src/entities/users/student.entity.ts +++ b/backend/src/entities/users/student.entity.ts @@ -13,12 +13,4 @@ export class Student extends User { @ManyToMany(() => Group) groups!: Collection; - - constructor( - public username: string, - public firstName: string, - public lastName: string - ) { - super(); - } } diff --git a/backend/src/entities/users/teacher.entity.ts b/backend/src/entities/users/teacher.entity.ts index 8e22d1de..d53ca603 100644 --- a/backend/src/entities/users/teacher.entity.ts +++ b/backend/src/entities/users/teacher.entity.ts @@ -7,12 +7,4 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js'; export class Teacher extends User { @ManyToMany(() => Class) classes!: Collection; - - constructor( - public username: string, - public firstName: string, - public lastName: string - ) { - super(); - } } diff --git a/backend/src/entities/users/user.entity.ts b/backend/src/entities/users/user.entity.ts index 1f35a0f8..15637110 100644 --- a/backend/src/entities/users/user.entity.ts +++ b/backend/src/entities/users/user.entity.ts @@ -6,8 +6,8 @@ export abstract class User { username!: string; @Property() - firstName: string = ''; + firstName = ''; @Property() - lastName: string = ''; + lastName = ''; } diff --git a/backend/src/exceptions.ts b/backend/src/exceptions.ts deleted file mode 100644 index e93a6c93..00000000 --- a/backend/src/exceptions.ts +++ /dev/null @@ -1,42 +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); - } -} diff --git a/backend/src/exceptions/bad-request-exception.ts b/backend/src/exceptions/bad-request-exception.ts new file mode 100644 index 00000000..f6672a62 --- /dev/null +++ b/backend/src/exceptions/bad-request-exception.ts @@ -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); + } +} diff --git a/backend/src/exceptions/conflict-exception.ts b/backend/src/exceptions/conflict-exception.ts new file mode 100644 index 00000000..ed1d0b24 --- /dev/null +++ b/backend/src/exceptions/conflict-exception.ts @@ -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); + } +} diff --git a/backend/src/exceptions/entity-already-exists-exception.ts b/backend/src/exceptions/entity-already-exists-exception.ts new file mode 100644 index 00000000..2769b814 --- /dev/null +++ b/backend/src/exceptions/entity-already-exists-exception.ts @@ -0,0 +1,7 @@ +import { ConflictException } from './conflict-exception.js'; + +export class EntityAlreadyExistsException extends ConflictException { + constructor(message: string) { + super(message); + } +} diff --git a/backend/src/exceptions/exception-with-http-state.ts b/backend/src/exceptions/exception-with-http-state.ts new file mode 100644 index 00000000..e5b9b9bd --- /dev/null +++ b/backend/src/exceptions/exception-with-http-state.ts @@ -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); + } +} diff --git a/backend/src/exceptions/forbidden-exception.ts b/backend/src/exceptions/forbidden-exception.ts new file mode 100644 index 00000000..4c58d1d5 --- /dev/null +++ b/backend/src/exceptions/forbidden-exception.ts @@ -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); + } +} diff --git a/backend/src/exceptions/not-found-exception.ts b/backend/src/exceptions/not-found-exception.ts new file mode 100644 index 00000000..a3e7d762 --- /dev/null +++ b/backend/src/exceptions/not-found-exception.ts @@ -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); + } +} diff --git a/backend/src/exceptions/unauthorized-exception.ts b/backend/src/exceptions/unauthorized-exception.ts new file mode 100644 index 00000000..54aa7cf9 --- /dev/null +++ b/backend/src/exceptions/unauthorized-exception.ts @@ -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); + } +} diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts index 8f6120b6..698b5b40 100644 --- a/backend/src/interfaces/assignment.ts +++ b/backend/src/interfaces/assignment.ts @@ -2,7 +2,8 @@ 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'; +import { getLogger } from '../logging/initalize.js'; export interface AssignmentDTO { id: number; @@ -46,7 +47,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; assignment.within = cls; - console.log(assignment); + getLogger().debug(assignment); return assignment; } diff --git a/backend/src/interfaces/class.ts b/backend/src/interfaces/class.ts index 371e3cae..ea1d4901 100644 --- a/backend/src/interfaces/class.ts +++ b/backend/src/interfaces/class.ts @@ -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 { diff --git a/backend/src/interfaces/learning-content.ts b/backend/src/interfaces/learning-content.ts index 51474917..693aec37 100644 --- a/backend/src/interfaces/learning-content.ts +++ b/backend/src/interfaces/learning-content.ts @@ -58,7 +58,7 @@ export interface EducationalGoal { export interface ReturnValue { callback_url: string; - callback_schema: Record; + callback_schema: Record; } export interface LearningObjectMetadata { diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 8cca42f6..0da87eb7 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -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; diff --git a/backend/src/interfaces/student.ts b/backend/src/interfaces/student.ts index 079b355b..ecce8f89 100644 --- a/backend/src/interfaces/student.ts +++ b/backend/src/interfaces/student.ts @@ -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, + }); } diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index fbaf520d..98cc4f22 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -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; diff --git a/backend/src/interfaces/teacher.ts b/backend/src/interfaces/teacher.ts index 4dd6adb4..31b4723f 100644 --- a/backend/src/interfaces/teacher.ts +++ b/backend/src/interfaces/teacher.ts @@ -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(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, + }); } diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts index 4d14e8ab..5c94a25f 100644 --- a/backend/src/logging/initalize.ts +++ b/backend/src/logging/initalize.ts @@ -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 { EnvVars, getEnvVar } from '../util/envvars.js'; +import { envVars, getEnvVar } from '../util/envVars.js'; export class Logger extends WinstonLogger { constructor() { @@ -9,7 +9,7 @@ export class Logger extends WinstonLogger { } } -const Labels: LokiLabels = { +const lokiLabels: LokiLabels = { source: 'Dwengo-Backend', service: 'API', host: 'localhost', @@ -22,28 +22,28 @@ function initializeLogger(): Logger { return logger; } - const logLevel = getEnvVar(EnvVars.LogLevel); + const logLevel = getEnvVar(envVars.LogLevel); const consoleTransport = new transports.Console({ - level: getEnvVar(EnvVars.LogLevel), + level: getEnvVar(envVars.LogLevel), format: format.combine(format.cli(), format.colorize()), }); - if (getEnvVar(EnvVars.RunMode) === 'dev') { + if (getEnvVar(envVars.RunMode) === 'dev') { return createLogger({ transports: [consoleTransport], }); } - const lokiHost = getEnvVar(EnvVars.LokiHost); + const lokiHost = getEnvVar(envVars.LokiHost); const lokiTransport: LokiTransport = new LokiTransport({ host: lokiHost, - labels: Labels, + labels: lokiLabels, level: logLevel, 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}`); }, diff --git a/backend/src/logging/mikroOrmLogger.ts b/backend/src/logging/mikroOrmLogger.ts index 25bbac13..9cb797a8 100644 --- a/backend/src/logging/mikroOrmLogger.ts +++ b/backend/src/logging/mikroOrmLogger.ts @@ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts'; export class MikroOrmLogger extends DefaultLogger { private logger: Logger = getLogger(); - log(namespace: LoggerNamespace, message: string, context?: LogContext) { + static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown { + const labels: LokiLabels = { + service: 'ORM', + }; + + let message: string; + if (context?.label) { + message = `[${namespace}] (${context.label}) ${messageArg}`; + } else { + message = `[${namespace}] ${messageArg}`; + } + + return { + message: message, + labels: labels, + context: context, + }; + } + + log(namespace: LoggerNamespace, message: string, context?: LogContext): void { if (!this.isEnabled(namespace, context)) { return; } switch (namespace) { case 'query': - this.logger.debug(this.createMessage(namespace, message, context)); + this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'query-params': // TODO Which log level should this be? - this.logger.info(this.createMessage(namespace, message, context)); + this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'schema': - this.logger.info(this.createMessage(namespace, message, context)); + this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'discovery': - this.logger.debug(this.createMessage(namespace, message, context)); + this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'info': - this.logger.info(this.createMessage(namespace, message, context)); + this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'deprecated': - this.logger.warn(this.createMessage(namespace, message, context)); + this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context)); break; default: switch (context?.level) { case 'info': - this.logger.info(this.createMessage(namespace, message, context)); + this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'warning': this.logger.warn(message); @@ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger { } } } - - private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) { - const labels: LokiLabels = { - service: 'ORM', - }; - - let message: string; - if (context?.label) { - message = `[${namespace}] (${context?.label}) ${messageArg}`; - } else { - message = `[${namespace}] ${messageArg}`; - } - - return { - message: message, - labels: labels, - context: context, - }; - } } diff --git a/backend/src/logging/responseTimeLogger.ts b/backend/src/logging/responseTimeLogger.ts index c1bb1e33..7fcc6c93 100644 --- a/backend/src/logging/responseTimeLogger.ts +++ b/backend/src/logging/responseTimeLogger.ts @@ -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; diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index 5ff5a53c..a91932ea 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -1,12 +1,13 @@ -import { EnvVars, getEnvVar } from '../../util/envvars.js'; +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'; +import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; +import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; const JWKS_CACHE = true; const JWKS_RATE_LIMIT = true; @@ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient { const idpConfigs = { student: { - issuer: getEnvVar(EnvVars.IdpStudentUrl), - jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), + issuer: getEnvVar(envVars.IdpStudentUrl), + jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)), }, teacher: { - issuer: getEnvVar(EnvVars.IdpTeacherUrl), - jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), + issuer: getEnvVar(envVars.IdpTeacherUrl), + jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)), }, }; @@ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({ } return signingKey.getPublicKey(); }, - audience: getEnvVar(EnvVars.IdpAudience), + audience: getEnvVar(envVars.IdpAudience), algorithms: [JWT_ALGORITHM], credentialsRequired: false, requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, @@ -74,7 +75,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 +85,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 +102,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 +115,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 +125,7 @@ export const authorize = next(); } }; +} /** * Middleware which rejects all unauthenticated users, but accepts all authenticated users. diff --git a/backend/src/middleware/auth/authentication-info.d.ts b/backend/src/middleware/auth/authentication-info.d.ts index 4b060dfa..e8f0d48c 100644 --- a/backend/src/middleware/auth/authentication-info.d.ts +++ b/backend/src/middleware/auth/authentication-info.d.ts @@ -1,11 +1,11 @@ /** * Object with information about the user who is currently logged in. */ -export type AuthenticationInfo = { +export interface AuthenticationInfo { accountType: 'student' | 'teacher'; username: string; name?: string; firstName?: string; lastName?: string; email?: string; -}; +} diff --git a/backend/src/middleware/cors.ts b/backend/src/middleware/cors.ts index 3d2c9be0..48e0704d 100644 --- a/backend/src/middleware/cors.ts +++ b/backend/src/middleware/cors.ts @@ -1,7 +1,7 @@ import cors from 'cors'; -import { EnvVars, getEnvVar } from '../util/envvars.js'; +import { envVars, getEnvVar } from '../util/envVars.js'; export default cors({ - origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','), - allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','), + origin: getEnvVar(envVars.CorsAllowedOrigins).split(','), + allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','), }); diff --git a/backend/src/middleware/error-handling/error-handler.ts b/backend/src/middleware/error-handling/error-handler.ts new file mode 100644 index 00000000..d7315603 --- /dev/null +++ b/backend/src/middleware/error-handling/error-handler.ts @@ -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); + } +} diff --git a/backend/src/mikro-orm.config.ts b/backend/src/mikro-orm.config.ts index f2fb2bae..eb0e5f7a 100644 --- a/backend/src/mikro-orm.config.ts +++ b/backend/src/mikro-orm.config.ts @@ -1,6 +1,6 @@ import { LoggerOptions, Options } from '@mikro-orm/core'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; -import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; +import { envVars, getEnvVar, getNumericEnvVar } from './util/envVars.js'; import { SqliteDriver } from '@mikro-orm/sqlite'; import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; @@ -42,33 +42,35 @@ const entities = [ Question, ]; -function config(testingMode: boolean = false): Options { +function config(testingMode = false): Options { if (testingMode) { return { driver: SqliteDriver, - dbName: getEnvVar(EnvVars.DbName), + 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) // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint) - dynamicImportProvider: (id) => import(id), + dynamicImportProvider: async (id) => import(id), }; } return { driver: PostgreSqlDriver, - host: getEnvVar(EnvVars.DbHost), - port: getNumericEnvVar(EnvVars.DbPort), - dbName: getEnvVar(EnvVars.DbName), - user: getEnvVar(EnvVars.DbUsername), - password: getEnvVar(EnvVars.DbPassword), + host: getEnvVar(envVars.DbHost), + port: getNumericEnvVar(envVars.DbPort), + dbName: getEnvVar(envVars.DbName), + 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 - debug: getEnvVar(EnvVars.LogLevel) === 'debug', + debug: getEnvVar(envVars.LogLevel) === 'debug', loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), }; } diff --git a/backend/src/orm.ts b/backend/src/orm.ts index 93feea7a..3e6e26c8 100644 --- a/backend/src/orm.ts +++ b/backend/src/orm.ts @@ -1,10 +1,10 @@ import { EntityManager, MikroORM } from '@mikro-orm/core'; import config from './mikro-orm.config.js'; -import { EnvVars, getEnvVar } from './util/envvars.js'; +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 = false): Promise { const logger: Logger = getLogger(); logger.info('Initializing ORM'); @@ -12,7 +12,7 @@ export async function initORM(testingMode: boolean = false) { orm = await MikroORM.init(config(testingMode)); // Update the database scheme if necessary and enabled. - if (getEnvVar(EnvVars.DbUpdate)) { + if (getEnvVar(envVars.DbUpdate)) { await orm.schema.updateSchema(); } else { const diff = await orm.schema.getUpdateSchemaSQL(); diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index a733d093..3652dcc6 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -19,7 +19,7 @@ router.get('/:id', getAssignmentHandler); router.get('/:id/submissions', getAssignmentsSubmissionsHandler); -router.get('/:id/questions', (req, res) => { +router.get('/:id/questions', (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 778e51fd..4a1f27d2 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -4,21 +4,21 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut const router = express.Router(); // Returns auth configuration for frontend -router.get('/config', (req, res) => { +router.get('/config', (_req, res) => { res.json(getFrontendAuthConfig()); }); -router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => { +router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ res.json({ message: 'If you see this, you should be authenticated!' }); }); -router.get('/testStudentsOnly', studentsOnly, (req, res) => { +router.get('/testStudentsOnly', studentsOnly, (_req, res) => { /* #swagger.security = [{ "student": [ ] }] */ res.json({ message: 'If you see this, you should be a student!' }); }); -router.get('/testTeachersOnly', teachersOnly, (req, res) => { +router.get('/testTeachersOnly', teachersOnly, (_req, res) => { /* #swagger.security = [{ "teacher": [ ] }] */ res.json({ message: 'If you see this, you should be a teacher!' }); }); diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index 0c9692b0..dc8917bd 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -14,7 +14,7 @@ router.get('/:groupid', getGroupHandler); router.get('/:groupid', getGroupSubmissionsHandler); // The list of questions a group has made -router.get('/:id/questions', (req, res) => { +router.get('/:id/questions', (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/router.ts b/backend/src/routes/router.ts index 639857a7..99d4312c 100644 --- a/backend/src/routes/router.ts +++ b/backend/src/routes/router.ts @@ -1,10 +1,7 @@ import { Response, Router } from 'express'; import studentRouter from './students.js'; -import groupRouter from './groups.js'; -import assignmentRouter from './assignments.js'; -import submissionRouter from './submissions.js'; +import teacherRouter from './teachers.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'; @@ -22,11 +19,8 @@ router.get('/', (_, res: Response) => { }); router.use('/student', studentRouter /* #swagger.tags = ['Student'] */); -router.use('/group', groupRouter /* #swagger.tags = ['Group'] */); -router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */); -router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */); +router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */); 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'] */); diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 7ed7a666..d58c2adc 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -9,7 +9,7 @@ import { getStudentHandler, getStudentSubmissionsHandler, } from '../controllers/students.js'; -import { getStudentGroups } from '../services/students.js'; + const router = express.Router(); // Root endpoint used to search objects @@ -37,7 +37,7 @@ router.get('/:id/assignments', getStudentAssignmentsHandler); router.get('/:id/groups', getStudentGroupsHandler); // A list of questions a user has created -router.get('/:id/questions', (req, res) => { +router.get('/:id/questions', (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts index 4db93027..8e9831b9 100644 --- a/backend/src/routes/submissions.ts +++ b/backend/src/routes/submissions.ts @@ -3,7 +3,7 @@ import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects -router.get('/', (req, res) => { +router.get('/', (_req, res) => { res.json({ submissions: ['0', '1'], }); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index c04e1575..3782a6ca 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -28,7 +28,7 @@ router.get('/:username/students', getTeacherStudentHandler); router.get('/:username/questions', getTeacherQuestionHandler); // Invitations to other classes a teacher received -router.get('/:id/invitations', (req, res) => { +router.get('/:id/invitations', (_req, res) => { res.json({ invitations: ['0'], }); diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts index be121810..22c5ce9e 100644 --- a/backend/src/services/assignments.ts +++ b/backend/src/services/assignments.ts @@ -1,7 +1,7 @@ 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'; +import { getLogger } from '../logging/initalize.js'; export async function getAllAssignments(classid: string, full: boolean): Promise { const classRepository = getClassRepository(); @@ -21,7 +21,7 @@ export async function getAllAssignments(classid: string, full: boolean): Promise return assignments.map(mapToAssignmentDTOId); } -export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise { +export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise { const classRepository = getClassRepository(); const cls = await classRepository.findById(classid); @@ -36,8 +36,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) { + getLogger().error(e); return null; } } @@ -60,7 +61,11 @@ export async function getAssignment(classid: string, id: number): Promise { +export async function getAssignmentsSubmissions( + classid: string, + assignmentNumber: number, + full: boolean +): Promise { const classRepository = getClassRepository(); const cls = await classRepository.findById(classid); @@ -79,7 +84,11 @@ 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); + if (full) { + return submissions.map(mapToSubmissionDTO); + } + + return submissions.map(mapToSubmissionDTOId); } diff --git a/backend/src/services/class.ts b/backend/src/services/classes.ts similarity index 86% rename from backend/src/services/class.ts rename to backend/src/services/classes.ts index 9f6e1efe..dc3ac8a0 100644 --- a/backend/src/services/class.ts +++ b/backend/src/services/classes.ts @@ -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,18 @@ export async function getAllClasses(full: boolean): Promise cls.classId!); } -export async function createClass(classData: ClassDTO): Promise { +export async function createClass(classData: ClassDTO): Promise { 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 cls = mapToClass(classData, teachers, students); + const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter( + (student) => student !== null + ); const classRepository = getClassRepository(); @@ -42,7 +43,7 @@ export async function createClass(classData: ClassDTO): Promise { }); await classRepository.save(newClass); - return newClass; + return mapToClassDTO(newClass); } catch (e) { logger.error(e); return null; diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 91091703..16895e0a 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -1,4 +1,3 @@ -import { GroupRepository } from '../data/assignments/group-repository.js'; import { getAssignmentRepository, getClassRepository, @@ -8,7 +7,8 @@ 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'; +import { getLogger } from '../logging/initalize.js'; export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise { const classRepository = getClassRepository(); @@ -43,9 +43,11 @@ 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 + ); - console.log(members); + getLogger().debug(members); const classRepository = getClassRepository(); const cls = await classRepository.findById(classid); @@ -71,7 +73,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme return newGroup; } catch (e) { - console.log(e); + getLogger().error(e); return null; } } @@ -95,15 +97,19 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu const groups = await groupRepository.findAllGroupsForAssignment(assignment); if (full) { - console.log('full'); - console.log(groups); + getLogger().debug({ full: full, groups: groups }); return groups.map(mapToGroupDTO); } return groups.map(mapToGroupDTOId); } -export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise { +export async function getGroupSubmissions( + classId: string, + assignmentNumber: number, + groupNumber: number, + full: boolean +): Promise { const classRepository = getClassRepository(); const cls = await classRepository.findById(classId); @@ -128,5 +134,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); } diff --git a/backend/src/services/learning-objects.ts b/backend/src/services/learning-objects.ts index fb579471..43af1aca 100644 --- a/backend/src/services/learning-objects.ts +++ b/backend/src/services/learning-objects.ts @@ -1,6 +1,7 @@ import { DWENGO_API_BASE } from '../config.js'; import { fetchWithLogging } from '../util/api-helper.js'; import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; +import { getLogger } from '../logging/initalize.js'; function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { return { @@ -37,7 +38,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr ); if (!metadata) { - console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`); return null; } @@ -45,6 +46,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 { + throw new Error('Function not implemented.'); +} + /** * Generic function to fetch learning objects (full data or just HRUIDs) */ @@ -53,7 +61,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`); if (!learningPathResponse.success || !learningPathResponse.data?.length) { - console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); + getLogger().error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); return []; } @@ -67,7 +75,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri objects.filter((obj): obj is FilteredLearningObject => obj !== null) ); } catch (error) { - console.error('❌ Error fetching learning objects:', error); + getLogger().error('❌ Error fetching learning objects:', error); return []; } } @@ -85,6 +93,3 @@ export async function getLearningObjectsFromPath(hruid: string, language: string export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise { return (await fetchLearningObjects(hruid, false, language)) as string[]; } -function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike { - throw new Error('Function not implemented.'); -} diff --git a/backend/src/services/learning-objects/attachment-service.ts b/backend/src/services/learning-objects/attachment-service.ts index aacc7187..4ff4ec47 100644 --- a/backend/src/services/learning-objects/attachment-service.ts +++ b/backend/src/services/learning-objects/attachment-service.ts @@ -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 { + async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise { const attachmentRepo = getAttachmentRepository(); if (learningObjectId.version) { diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts index bab0b9b1..a8055f2c 100644 --- a/backend/src/services/learning-objects/database-learning-object-provider.ts +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -1,7 +1,6 @@ import { LearningObjectProvider } from './learning-object-provider.js'; import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js'; -import { Language } from '../../entities/content/language.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { getUrlStringForLearningObject } from '../../util/links.js'; import processingService from './processing/processing-service.js'; @@ -41,10 +40,10 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL }; } -function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise { +async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise { const learningObjectRepo = getLearningObjectRepository(); - return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); + return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); } /** @@ -65,11 +64,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { const learningObjectRepo = getLearningObjectRepository(); - const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); + const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); if (!learningObject) { return null; } - return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id)); + return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id)); }, /** @@ -96,7 +95,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, diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 8289660b..59ffb643 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -1,11 +1,11 @@ import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js'; import { LearningObjectProvider } from './learning-object-provider.js'; -import { EnvVars, getEnvVar } from '../../util/envvars.js'; +import { envVars, getEnvVar } from '../../util/envVars.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js'; function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { - if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { + if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { return databaseLearningObjectProvider; } return dwengoApiLearningObjectProvider; @@ -18,28 +18,28 @@ const learningObjectService = { /** * Fetches a single learning object by its HRUID */ - getLearningObjectById(id: LearningObjectIdentifier): Promise { + async getLearningObjectById(id: LearningObjectIdentifier): Promise { return getProvider(id).getLearningObjectById(id); }, /** * Fetch full learning object data (metadata) */ - getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { + async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { return getProvider(id).getLearningObjectsFromPath(id); }, /** * Fetch only learning object HRUIDs */ - getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { return getProvider(id).getLearningObjectIdsFromPath(id); }, /** * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ - getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { return getProvider(id).getLearningObjectHTML(id); }, }; diff --git a/backend/src/services/learning-objects/processing/audio/audio-processor.ts b/backend/src/services/learning-objects/processing/audio/audio-processor.ts index 592669d5..227eae13 100644 --- a/backend/src/services/learning-objects/processing/audio/audio-processor.ts +++ b/backend/src/services/learning-objects/processing/audio/audio-processor.ts @@ -14,7 +14,7 @@ class AudioProcessor extends StringProcessor { super(DwengoContentType.AUDIO_MPEG); } - protected renderFn(audioUrl: string): string { + override renderFn(audioUrl: string): string { return DOMPurify.sanitize(`