diff --git a/backend/config.js b/backend/config.js new file mode 100644 index 00000000..be42027c --- /dev/null +++ b/backend/config.js @@ -0,0 +1,7 @@ +// Can be placed in dotenv but found it redundant +// Import dotenv from "dotenv"; +// Load .env file +// Dotenv.config(); +export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; +export const FALLBACK_LANG = 'nl'; +export const FALLBACK_SEQ_NUM = 1; diff --git a/backend/src/config.ts b/backend/src/config.ts index 6cf388cc..69af5d74 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -1,4 +1,5 @@ import { EnvVars, getEnvVar } from './util/envvars.js'; +import { Language } from './entities/content/language.js'; // API export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); @@ -7,3 +8,5 @@ export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); // Logging export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info'; export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102'; + +export const FALLBACK_SEQ_NUM = 1; diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts new file mode 100644 index 00000000..03332469 --- /dev/null +++ b/backend/src/controllers/assignments.ts @@ -0,0 +1,76 @@ +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 +interface AssignmentParams { + classid: string; + id: string; +} + +export async function getAllAssignmentsHandler(req: Request, res: Response): Promise { + const classid = req.params.classid; + const full = req.query.full === 'true'; + + const assignments = await getAllAssignments(classid, full); + + res.json({ + assignments: assignments, + }); +} + +export async function createAssignmentHandler(req: Request, res: Response): Promise { + const classid = req.params.classid; + const assignmentData = req.body as AssignmentDTO; + + if (!assignmentData.description || !assignmentData.language || !assignmentData.learningPath || !assignmentData.title) { + res.status(400).json({ + error: 'Missing one or more required fields: title, description, learningPath, language', + }); + return; + } + + const assignment = await createAssignment(classid, assignmentData); + + if (!assignment) { + res.status(500).json({ error: 'Could not create assignment ' }); + return; + } + + res.status(201).json({ assignment: assignment }); +} + +export async function getAssignmentHandler(req: Request, res: Response): Promise { + const id = +req.params.id; + const classid = req.params.classid; + + if (isNaN(id)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const assignment = await getAssignment(classid, id); + + if (!assignment) { + res.status(404).json({ error: 'Assignment not found' }); + return; + } + + res.json(assignment); +} + +export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise { + const classid = req.params.classid; + const assignmentNumber = +req.params.id; + + if (isNaN(assignmentNumber)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const submissions = await getAssignmentsSubmissions(classid, assignmentNumber); + + res.json({ + submissions: submissions, + }); +} diff --git a/backend/src/controllers/classes.ts b/backend/src/controllers/classes.ts new file mode 100644 index 00000000..ca2f5698 --- /dev/null +++ b/backend/src/controllers/classes.ts @@ -0,0 +1,77 @@ +import { Request, Response } from 'express'; +import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js'; +import { ClassDTO } from '../interfaces/class.js'; + +export async function getAllClassesHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + const classes = await getAllClasses(full); + + res.json({ + classes: classes, + }); +} + +export async function createClassHandler(req: Request, res: Response): Promise { + const classData = req.body as ClassDTO; + + if (!classData.displayName) { + res.status(400).json({ + error: 'Missing one or more required fields: displayName', + }); + return; + } + + const cls = await createClass(classData); + + if (!cls) { + res.status(500).json({ error: 'Something went wrong while creating class' }); + return; + } + + res.status(201).json({ class: cls }); +} + +export async function getClassHandler(req: Request, res: Response): Promise { + try { + 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' }); + } +} + +export async function getClassStudentsHandler(req: Request, res: Response): Promise { + const classId = req.params.id; + const full = req.query.full === 'true'; + + const students = full ? await getClassStudents(classId) : await getClassStudentsIds(classId); + + res.json({ + students: students, + }); +} + +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 invitations = await getClassTeacherInvitations(classId, full); + + res.json({ + invitations: invitations, + }); +} diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts new file mode 100644 index 00000000..b7bfd212 --- /dev/null +++ b/backend/src/controllers/groups.ts @@ -0,0 +1,95 @@ +import { Request, Response } from 'express'; +import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; +import { GroupDTO } from '../interfaces/group.js'; + +// Typescript is annoywith with parameter forwarding from class.ts +interface GroupParams { + classid: string; + assignmentid: string; + groupid?: string; +} + +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; + + if (isNaN(assignmentId)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const groupId = +req.params.groupid!; // Can't be undefined + + if (isNaN(groupId)) { + res.status(400).json({ error: 'Group id must be a number' }); + return; + } + + const group = await getGroup(classId, assignmentId, groupId, full); + + res.json(group); +} + +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; + + if (isNaN(assignmentId)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const groups = await getAllGroups(classId, assignmentId, full); + + res.json({ + groups: groups, + }); +} + +export async function createGroupHandler(req: Request, res: Response): Promise { + const classid = req.params.classid; + const assignmentId = +req.params.assignmentid; + + if (isNaN(assignmentId)) { + res.status(400).json({ error: 'Assignment id must be a number' }); + return; + } + + const groupData = req.body as GroupDTO; + const group = await createGroup(groupData, classid, assignmentId); + + if (!group) { + res.status(500).json({ error: 'Something went wrong while creating group' }); + return; + } + + res.status(201).json({ group: group }); +} + +export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise { + const classId = req.params.classid; + // Const full = req.query.full === 'true'; + + const assignmentId = +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 + + if (isNaN(groupId)) { + res.status(400).json({ error: 'Group id must be a number' }); + return; + } + + const submissions = await getGroupSubmissions(classId, assignmentId, groupId); + + res.json({ + submissions: submissions, + }); +} diff --git a/backend/src/controllers/learningObjects.ts b/backend/src/controllers/learningObjects.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/src/controllers/learningPaths.ts b/backend/src/controllers/learningPaths.ts deleted file mode 100644 index 0a7ff0ae..00000000 --- a/backend/src/controllers/learningPaths.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Request, Response } from 'express'; -import { themes } from '../data/themes.js'; -import { FALLBACK_LANG } from '../config.js'; -import { getLogger } from '../logging/initalize.js'; -import learningPathService from '../services/learning-paths/learning-path-service.js'; -import { Language } from '../entities/content/language.js'; -/** - * Fetch learning paths based on query parameters. - */ -export async function getLearningPaths(req: Request, res: Response): Promise { - try { - const hruids = req.query.hruid; - const themeKey = req.query.theme as string; - const searchQuery = req.query.search as string; - const language = (req.query.language as Language) || FALLBACK_LANG; - - let hruidList; - - if (hruids) { - hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)]; - } else if (themeKey) { - const theme = themes.find((t) => t.title === themeKey); - if (theme) { - hruidList = theme.hruids; - } else { - res.status(404).json({ - error: `Theme "${themeKey}" not found.`, - }); - return; - } - } else if (searchQuery) { - const searchResults = await learningPathService.searchLearningPaths(searchQuery, language); - res.json(searchResults); - return; - } else { - hruidList = themes.flatMap((theme) => theme.hruids); - } - - const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`); - res.json(learningPaths.data); - } catch (error) { - getLogger().error('❌ Unexpected error fetching learning paths:', error); - res.status(500).json({ error: 'Internal server error' }); - } -} diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts new file mode 100644 index 00000000..917b48ae --- /dev/null +++ b/backend/src/controllers/questions.ts @@ -0,0 +1,119 @@ +import { Request, Response } from 'express'; +import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; +import { QuestionDTO, QuestionId } from '../interfaces/question.js'; +import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; +import { Language } from '../entities/content/language.js'; + +function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { + const { hruid, version } = req.params; + const lang = req.query.lang; + + if (!hruid || !version) { + res.status(400).json({ error: 'Missing required parameters.' }); + return null; + } + + return { + hruid, + language: (lang as Language) || FALLBACK_LANG, + version: +version, + }; +} + +function getQuestionId(req: Request, res: Response): QuestionId | null { + const seq = req.params.seq; + const learningObjectIdentifier = getObjectId(req, res); + + if (!learningObjectIdentifier) { + return null; + } + + return { + learningObjectIdentifier, + sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, + }; +} + +export async function getAllQuestionsHandler(req: Request, res: Response): Promise { + const objectId = getObjectId(req, res); + const full = req.query.full === 'true'; + + if (!objectId) { + return; + } + + const questions = await getAllQuestions(objectId, full); + + if (!questions) { + res.status(404).json({ error: `Questions not found.` }); + } else { + res.json(questions); + } +} + +export async function getQuestionHandler(req: Request, res: Response): Promise { + const questionId = getQuestionId(req, res); + + if (!questionId) { + return; + } + + const question = await getQuestion(questionId); + + if (!question) { + res.status(404).json({ error: `Question not found.` }); + } else { + res.json(question); + } +} + +export async function getQuestionAnswersHandler(req: Request, res: Response): Promise { + const questionId = getQuestionId(req, res); + const full = req.query.full === 'true'; + + if (!questionId) { + return; + } + + const answers = getAnswersByQuestion(questionId, full); + + if (!answers) { + res.status(404).json({ error: `Questions not found.` }); + } else { + res.json(answers); + } +} + +export async function createQuestionHandler(req: Request, res: Response): Promise { + const questionDTO = req.body as QuestionDTO; + + if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) { + res.status(400).json({ error: 'Missing required fields: identifier and content' }); + return; + } + + const question = await createQuestion(questionDTO); + + if (!question) { + res.status(400).json({ error: 'Could not add question' }); + } else { + res.json(question); + } +} + +export async function deleteQuestionHandler(req: Request, res: Response): Promise { + const questionId = getQuestionId(req, res); + + if (!questionId) { + return; + } + + const question = await deleteQuestion(questionId); + + if (!question) { + res.status(400).json({ error: 'Could not find nor delete question' }); + } else { + res.json(question); + } +} diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts new file mode 100644 index 00000000..6c253cff --- /dev/null +++ b/backend/src/controllers/students.ts @@ -0,0 +1,146 @@ +import { Request, Response } from 'express'; +import { + createStudent, + deleteStudent, + getAllStudents, + getStudent, + getStudentAssignments, + getStudentClasses, + 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(); + + if (!students) { + res.status(404).json({ error: `Student not found.` }); + return; + } + + res.status(201).json(students); +} + +export async function getStudentHandler(req: Request, res: Response): Promise { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const user = await getStudent(username); + + if (!user) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(201).json(user); +} + +export async function createStudentHandler(req: Request, res: Response) { + const userData = req.body as StudentDTO; + + if (!userData.username || !userData.firstName || !userData.lastName) { + res.status(400).json({ + error: 'Missing required fields: username, firstName, lastName', + }); + return; + } + + const newUser = await createStudent(userData); + res.status(201).json(newUser); +} + +export async function deleteStudentHandler(req: Request, res: Response) { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const deletedUser = await deleteStudent(username); + if (!deletedUser) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(200).json(deletedUser); +} + +export async function getStudentClassesHandler(req: Request, res: Response): Promise { + try { + const full = req.query.full === 'true'; + const username = req.params.id; + + 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' }); + } +} + +// TODO +// Might not be fully correct depending on if +// A class has an assignment, that all students +// Have this assignment. +export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + const username = req.params.id; + + const assignments = getStudentAssignments(username, full); + + res.json({ + assignments: assignments, + }); +} + +export async function getStudentGroupsHandler(req: Request, res: Response): Promise { + const full = req.query.full === 'true'; + const username = req.params.id; + + const groups = await getStudentGroups(username, full); + + res.json({ + groups: groups, + }); +} + +export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise { + const username = req.params.id; + + const submissions = await getStudentSubmissions(username); + + res.json({ + submissions: submissions, + }); +} diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts new file mode 100644 index 00000000..1e66dbe9 --- /dev/null +++ b/backend/src/controllers/submissions.ts @@ -0,0 +1,59 @@ +import { Request, Response } from 'express'; +import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; +import { Language, languageMap } from '../entities/content/language.js'; +import { SubmissionDTO } from '../interfaces/submission'; + +interface SubmissionParams { + hruid: string; + id: number; +} + +export async function getSubmissionHandler(req: Request, res: Response): Promise { + const lohruid = req.params.hruid; + const submissionNumber = +req.params.id; + + if (isNaN(submissionNumber)) { + res.status(400).json({ error: 'Submission number is not a number' }); + return; + } + + const lang = languageMap[req.query.language as string] || Language.Dutch; + const version = (req.query.version || 1) as number; + + const submission = await getSubmission(lohruid, lang, version, submissionNumber); + + if (!submission) { + res.status(404).json({ error: 'Submission not found' }); + return; + } + + res.json(submission); +} + +export async function createSubmissionHandler(req: Request, res: Response) { + 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); + } +} + +export async function deleteSubmissionHandler(req: Request, res: Response) { + const hruid = req.params.hruid; + const submissionNumber = +req.params.id; + + const lang = languageMap[req.query.language as string] || Language.Dutch; + const version = (req.query.version || 1) as number; + + const submission = await deleteSubmission(hruid, lang, version, submissionNumber); + + if (!submission) { + res.status(404).json({ error: 'Submission not found' }); + } else { + res.json(submission); + } +} diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts new file mode 100644 index 00000000..52e5e713 --- /dev/null +++ b/backend/src/controllers/teachers.ts @@ -0,0 +1,144 @@ +import { Request, Response } from 'express'; +import { + createTeacher, + 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(); + + if (!teachers) { + res.status(404).json({ error: `Teacher not found.` }); + return; + } + + res.status(201).json(teachers); +} + +export async function getTeacherHandler(req: Request, res: Response): Promise { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const user = await getTeacher(username); + + if (!user) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(201).json(user); +} + +export async function createTeacherHandler(req: Request, res: Response) { + const userData = req.body as TeacherDTO; + + if (!userData.username || !userData.firstName || !userData.lastName) { + res.status(400).json({ + error: 'Missing required fields: username, firstName, lastName', + }); + return; + } + + const newUser = await createTeacher(userData); + res.status(201).json(newUser); +} + +export async function deleteTeacherHandler(req: Request, res: Response) { + const username = req.params.username; + + if (!username) { + res.status(400).json({ error: 'Missing required field: username' }); + return; + } + + const deletedUser = await deleteTeacher(username); + if (!deletedUser) { + res.status(404).json({ + error: `User with username '${username}' not found.`, + }); + return; + } + + res.status(200).json(deletedUser); +} + +export async function getTeacherClassHandler(req: Request, res: Response): Promise { + try { + const username = req.params.username as string; + 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' }); + } +} + +export async function getTeacherStudentHandler(req: Request, res: Response): Promise { + try { + const username = req.params.username as string; + 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' }); + } +} + +export async function getTeacherQuestionHandler(req: Request, res: Response): Promise { + try { + const username = req.params.username as string; + 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' }); + } +} diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index fe1eb818..61a1a834 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; -import { loadTranslations } from '../util/translationHelper.js'; +import { loadTranslations } from '../util/translation-helper.js'; interface Translations { curricula_page: { diff --git a/backend/src/controllers/users.ts b/backend/src/controllers/users.ts new file mode 100644 index 00000000..850c6549 --- /dev/null +++ b/backend/src/controllers/users.ts @@ -0,0 +1,91 @@ +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/group-repository.ts b/backend/src/data/assignments/group-repository.ts index df92eaae..eb1b09e2 100644 --- a/backend/src/data/assignments/group-repository.ts +++ b/backend/src/data/assignments/group-repository.ts @@ -1,16 +1,26 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Group } from '../../entities/assignments/group.entity.js'; 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 { - return this.findOne({ - assignment: assignment, - groupNumber: groupNumber, - }); + return this.findOne( + { + assignment: assignment, + groupNumber: groupNumber, + }, + { populate: ['members'] } + ); } public findAllGroupsForAssignment(assignment: Assignment): Promise { - return this.findAll({ where: { assignment: assignment } }); + return this.findAll({ + where: { assignment: assignment }, + populate: ['members'], + }); + } + public findAllGroupsWithStudent(student: Student): Promise { + return this.find({ members: student }, { populate: ['members'] }); } public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { return this.deleteWhere({ diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index faa9fef1..251823fa 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -38,6 +38,14 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } + public findAllSubmissionsForGroup(group: Group): Promise { + return this.find({ onBehalfOf: group }); + } + + public findAllSubmissionsForStudent(student: Student): Promise { + return this.find({ submitter: student }); + } + public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { return this.deleteWhere({ learningObjectHruid: loId.hruid, diff --git a/backend/src/data/classes/class-repository.ts b/backend/src/data/classes/class-repository.ts index e3b9f959..0ceed98e 100644 --- a/backend/src/data/classes/class-repository.ts +++ b/backend/src/data/classes/class-repository.ts @@ -1,11 +1,23 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Class } from '../../entities/classes/class.entity.js'; +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 { - return this.findOne({ classId: id }); + return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); } public deleteById(id: string): Promise { return this.deleteWhere({ classId: id }); } + public findByStudent(student: Student): Promise { + return this.find( + { students: student }, + { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe + ); + } + + public findByTeacher(teacher: Teacher): Promise { + return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); + } } diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index f9b6bfcb..49b4c536 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -1,7 +1,8 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; -import { Language } from '../../entities/content/language'; +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 { @@ -31,4 +32,11 @@ export class LearningObjectRepository extends DwengoEntityRepository { + return this.find( + { admins: teacher }, + { populate: ['admins'] } // Make sure to load admin relations + ); + } } diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 4099c528..9207e1dd 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -2,6 +2,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Question } from '../../entities/questions/question.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; 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 { @@ -40,4 +41,17 @@ export class QuestionRepository extends DwengoEntityRepository { sequenceNumber: sequenceNumber, }); } + + public async findAllByLearningObjects(learningObjects: LearningObject[]): Promise { + const objectIdentifiers = learningObjects.map((lo) => ({ + learningObjectHruid: lo.hruid, + learningObjectLanguage: lo.language, + learningObjectVersion: lo.version, + })); + + return this.findAll({ + where: { $or: objectIdentifiers }, + orderBy: { timestamp: 'ASC' }, + }); + } } diff --git a/backend/src/data/repositories.ts b/backend/src/data/repositories.ts index 3daa026d..02385109 100644 --- a/backend/src/data/repositories.ts +++ b/backend/src/data/repositories.ts @@ -54,7 +54,6 @@ function repositoryGetter>(en } /* Users */ -export const getUserRepository = repositoryGetter(User); export const getStudentRepository = repositoryGetter(Student); export const getTeacherRepository = repositoryGetter(Teacher); diff --git a/backend/src/data/users/student-repository.ts b/backend/src/data/users/student-repository.ts index 1c3a6fae..0792678d 100644 --- a/backend/src/data/users/student-repository.ts +++ b/backend/src/data/users/student-repository.ts @@ -1,5 +1,9 @@ -import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; 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 { diff --git a/backend/src/data/users/teacher-repository.ts b/backend/src/data/users/teacher-repository.ts index 704ef409..2b2bee75 100644 --- a/backend/src/data/users/teacher-repository.ts +++ b/backend/src/data/users/teacher-repository.ts @@ -1,5 +1,6 @@ -import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; 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 { diff --git a/backend/src/data/users/user-repository.ts b/backend/src/data/users/user-repository.ts index 7e2a42ad..21497b79 100644 --- a/backend/src/data/users/user-repository.ts +++ b/backend/src/data/users/user-repository.ts @@ -1,11 +1,11 @@ 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 { - return this.findOne({ username: username }); +export class UserRepository extends DwengoEntityRepository { + public findByUsername(username: string): Promise { + return this.findOne({ username } as Partial); } public deleteByUsername(username: string): Promise { - return this.deleteWhere({ username: username }); + 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 cda27d66..692e2112 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -1,16 +1,21 @@ -import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; +import { Collection, 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'; import { AssignmentRepository } from '../../data/assignments/assignment-repository.js'; -@Entity({ repository: () => AssignmentRepository }) +@Entity({ + repository: () => AssignmentRepository, +}) export class Assignment { - @ManyToOne({ entity: () => Class, primary: true }) + @ManyToOne({ + entity: () => Class, + primary: true, + }) within!: Class; - @PrimaryKey({ type: 'number' }) - id!: number; + @PrimaryKey({ type: 'number', autoincrement: true }) + id?: number; @Property({ type: 'string' }) title!: string; @@ -21,9 +26,14 @@ export class Assignment { @Property({ type: 'string' }) learningPathHruid!: string; - @Enum({ items: () => Language }) + @Enum({ + items: () => Language, + }) learningPathLanguage!: Language; - @OneToMany({ entity: () => Group, mappedBy: 'assignment' }) + @OneToMany({ + entity: () => Group, + mappedBy: 'assignment', + }) groups!: Group[]; } diff --git a/backend/src/entities/assignments/group.entity.ts b/backend/src/entities/assignments/group.entity.ts index 632ad722..213e0f38 100644 --- a/backend/src/entities/assignments/group.entity.ts +++ b/backend/src/entities/assignments/group.entity.ts @@ -1,9 +1,11 @@ -import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; +import { Collection, 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'; -@Entity({ repository: () => GroupRepository }) +@Entity({ + repository: () => GroupRepository, +}) export class Group { @ManyToOne({ entity: () => Assignment, @@ -11,8 +13,8 @@ export class Group { }) assignment!: Assignment; - @PrimaryKey({ type: 'integer' }) - groupNumber!: number; + @PrimaryKey({ type: 'integer', autoincrement: true }) + groupNumber?: number; @ManyToMany({ entity: () => Student, diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index f6f8b3c7..f008c8c2 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -18,7 +18,7 @@ export class Submission { @PrimaryKey({ type: 'numeric' }) learningObjectVersion: number = 1; - @PrimaryKey({ type: 'integer' }) + @PrimaryKey({ type: 'integer', autoincrement: true }) submissionNumber!: number; @ManyToOne({ diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index 62ed37d0..bdef1f52 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -3,7 +3,9 @@ import { Student } from '../users/student.entity.js'; import { Class } from './class.entity.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; -@Entity({ repository: () => ClassJoinRequestRepository }) +@Entity({ + repository: () => ClassJoinRequestRepository, +}) export class ClassJoinRequest { @ManyToOne({ entity: () => Student, diff --git a/backend/src/entities/classes/class.entity.ts b/backend/src/entities/classes/class.entity.ts index d4e44bf9..63315304 100644 --- a/backend/src/entities/classes/class.entity.ts +++ b/backend/src/entities/classes/class.entity.ts @@ -4,10 +4,12 @@ import { Teacher } from '../users/teacher.entity.js'; import { Student } from '../users/student.entity.js'; import { ClassRepository } from '../../data/classes/class-repository.js'; -@Entity({ repository: () => ClassRepository }) +@Entity({ + repository: () => ClassRepository, +}) export class Class { @PrimaryKey() - classId = v4(); + classId? = v4(); @Property({ type: 'string' }) displayName!: string; diff --git a/backend/src/entities/classes/teacher-invitation.entity.ts b/backend/src/entities/classes/teacher-invitation.entity.ts index eb57d98a..668a0a1c 100644 --- a/backend/src/entities/classes/teacher-invitation.entity.ts +++ b/backend/src/entities/classes/teacher-invitation.entity.ts @@ -7,9 +7,6 @@ import { TeacherInvitationRepository } from '../../data/classes/teacher-invitati * Invitation of a teacher into a class (in order to teach it). */ @Entity({ repository: () => TeacherInvitationRepository }) -@Entity({ - repository: () => TeacherInvitationRepository, -}) export class TeacherInvitation { @ManyToOne({ entity: () => Teacher, diff --git a/backend/src/entities/content/attachment.entity.ts b/backend/src/entities/content/attachment.entity.ts index 0c0f53c4..80104f28 100644 --- a/backend/src/entities/content/attachment.entity.ts +++ b/backend/src/entities/content/attachment.entity.ts @@ -2,7 +2,9 @@ import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; import { LearningObject } from './learning-object.entity.js'; import { AttachmentRepository } from '../../data/content/attachment-repository.js'; -@Entity({ repository: () => AttachmentRepository }) +@Entity({ + repository: () => AttachmentRepository, +}) export class Attachment { @ManyToOne({ entity: () => LearningObject, diff --git a/backend/src/entities/content/language.ts b/backend/src/entities/content/language.ts index d7687331..7e7b42d2 100644 --- a/backend/src/entities/content/language.ts +++ b/backend/src/entities/content/language.ts @@ -184,3 +184,10 @@ export enum Language { Zhuang = 'za', Zulu = 'zu', } + +export const languageMap: Record = { + nl: Language.Dutch, + fr: Language.French, + en: Language.English, + de: Language.German, +}; diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts new file mode 100644 index 00000000..493fd3c0 --- /dev/null +++ b/backend/src/interfaces/answer.ts @@ -0,0 +1,38 @@ +import { mapToUserDTO, UserDTO } from './user.js'; +import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js'; +import { Answer } from '../entities/questions/answer.entity.js'; + +export interface AnswerDTO { + author: UserDTO; + toQuestion: QuestionDTO; + sequenceNumber: number; + timestamp: string; + content: string; +} + +/** + * Convert a Question entity to a DTO format. + */ +export function mapToAnswerDTO(answer: Answer): AnswerDTO { + return { + author: mapToUserDTO(answer.author), + toQuestion: mapToQuestionDTO(answer.toQuestion), + sequenceNumber: answer.sequenceNumber!, + timestamp: answer.timestamp.toISOString(), + content: answer.content, + }; +} + +export interface AnswerId { + author: string; + toQuestion: QuestionId; + sequenceNumber: number; +} + +export function mapToAnswerId(answer: AnswerDTO): AnswerId { + return { + author: answer.author.username, + toQuestion: mapToQuestionId(answer.toQuestion), + sequenceNumber: answer.sequenceNumber, + }; +} diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts new file mode 100644 index 00000000..8f6120b6 --- /dev/null +++ b/backend/src/interfaces/assignment.ts @@ -0,0 +1,52 @@ +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'; + +export interface AssignmentDTO { + id: number; + class: string; // Id of class 'within' + title: string; + description: string; + learningPath: string; + language: string; + groups?: GroupDTO[] | string[]; // TODO +} + +export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { + return { + id: assignment.id!, + class: assignment.within.classId!, + title: assignment.title, + description: assignment.description, + learningPath: assignment.learningPathHruid, + language: assignment.learningPathLanguage, + // Groups: assignment.groups.map(group => group.groupNumber), + }; +} + +export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO { + return { + id: assignment.id!, + class: assignment.within.classId!, + title: assignment.title, + description: assignment.description, + learningPath: assignment.learningPathHruid, + language: assignment.learningPathLanguage, + // Groups: assignment.groups.map(mapToGroupDTO), + }; +} + +export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment { + const assignment = new Assignment(); + assignment.title = assignmentData.title; + assignment.description = assignmentData.description; + assignment.learningPathHruid = assignmentData.learningPath; + assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; + assignment.within = cls; + + console.log(assignment); + + return assignment; +} diff --git a/backend/src/interfaces/class.ts b/backend/src/interfaces/class.ts new file mode 100644 index 00000000..371e3cae --- /dev/null +++ b/backend/src/interfaces/class.ts @@ -0,0 +1,37 @@ +import { Collection } from '@mikro-orm/core'; +import { Class } from '../entities/classes/class.entity.js'; +import { Student } from '../entities/users/student.entity.js'; +import { Teacher } from '../entities/users/teacher.entity.js'; + +export interface ClassDTO { + id: string; + displayName: string; + teachers: string[]; + students: string[]; + joinRequests: string[]; + endpoints?: { + self: string; + invitations: string; + assignments: string; + students: string; + }; +} + +export function mapToClassDTO(cls: Class): ClassDTO { + return { + id: cls.classId!, + displayName: cls.displayName, + teachers: cls.teachers.map((teacher) => teacher.username), + students: cls.students.map((student) => student.username), + joinRequests: [], // TODO + }; +} + +export function mapToClass(classData: ClassDTO, students: Collection, teachers: Collection): Class { + const cls = new Class(); + cls.displayName = classData.displayName; + cls.students = students; + cls.teachers = teachers; + + return cls; +} diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts new file mode 100644 index 00000000..a25c5b8e --- /dev/null +++ b/backend/src/interfaces/group.ts @@ -0,0 +1,25 @@ +import { Group } from '../entities/assignments/group.entity.js'; +import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js'; +import { mapToStudentDTO, StudentDTO } from './student.js'; + +export interface GroupDTO { + assignment: number | AssignmentDTO; + groupNumber: number; + members: string[] | StudentDTO[]; +} + +export function mapToGroupDTO(group: Group): GroupDTO { + return { + assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within), + groupNumber: group.groupNumber!, + members: group.members.map(mapToStudentDTO), + }; +} + +export function mapToGroupDTOId(group: Group): GroupDTO { + return { + assignment: group.assignment.id!, + groupNumber: group.groupNumber!, + members: group.members.map((member) => member.username), + }; +} diff --git a/backend/src/interfaces/list.ts b/backend/src/interfaces/list.ts new file mode 100644 index 00000000..6892fb9d --- /dev/null +++ b/backend/src/interfaces/list.ts @@ -0,0 +1,5 @@ +// TODO: implement something like this but with named endpoints +export interface List { + items: T[]; + endpoints?: string[]; +} diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts new file mode 100644 index 00000000..8cca42f6 --- /dev/null +++ b/backend/src/interfaces/question.ts @@ -0,0 +1,44 @@ +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; + sequenceNumber?: number; + author: StudentDTO; + timestamp?: string; + content: string; +} + +/** + * Convert a Question entity to a DTO format. + */ +export function mapToQuestionDTO(question: Question): QuestionDTO { + const learningObjectIdentifier = { + hruid: question.learningObjectHruid, + language: question.learningObjectLanguage, + version: question.learningObjectVersion, + }; + + return { + learningObjectIdentifier, + sequenceNumber: question.sequenceNumber!, + author: mapToStudentDTO(question.author), + timestamp: question.timestamp.toISOString(), + content: question.content, + }; +} + +export interface QuestionId { + learningObjectIdentifier: LearningObjectIdentifier; + sequenceNumber: number; +} + +export function mapToQuestionId(question: QuestionDTO): QuestionId { + return { + learningObjectIdentifier: question.learningObjectIdentifier, + sequenceNumber: question.sequenceNumber!, + }; +} diff --git a/backend/src/interfaces/student.ts b/backend/src/interfaces/student.ts new file mode 100644 index 00000000..079b355b --- /dev/null +++ b/backend/src/interfaces/student.ts @@ -0,0 +1,29 @@ +import { Student } from '../entities/users/student.entity.js'; + +export interface StudentDTO { + id: string; + username: string; + firstName: string; + lastName: string; + endpoints?: { + classes: string; + questions: string; + invitations: string; + groups: string; + }; +} + +export function mapToStudentDTO(student: Student): StudentDTO { + return { + id: student.username, + username: student.username, + firstName: student.firstName, + lastName: student.lastName, + }; +} + +export function mapToStudent(studentData: StudentDTO): Student { + const student = new Student(studentData.username, studentData.firstName, studentData.lastName); + + return student; +} diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts new file mode 100644 index 00000000..fbaf520d --- /dev/null +++ b/backend/src/interfaces/submission.ts @@ -0,0 +1,47 @@ +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'; + +export interface SubmissionDTO { + learningObjectHruid: string; + learningObjectLanguage: Language; + learningObjectVersion: number; + + submissionNumber?: number; + submitter: StudentDTO; + time?: Date; + group?: GroupDTO; + content: string; +} + +export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { + return { + learningObjectHruid: submission.learningObjectHruid, + learningObjectLanguage: submission.learningObjectLanguage, + learningObjectVersion: submission.learningObjectVersion, + + submissionNumber: submission.submissionNumber, + submitter: mapToStudentDTO(submission.submitter), + time: submission.submissionTime, + group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined, + content: submission.content, + }; +} + +export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { + const submission = new Submission(); + submission.learningObjectHruid = submissionDTO.learningObjectHruid; + submission.learningObjectLanguage = submissionDTO.learningObjectLanguage; + submission.learningObjectVersion = submissionDTO.learningObjectVersion; + // Submission.submissionNumber = submissionDTO.submissionNumber; + submission.submitter = mapToStudent(submissionDTO.submitter); + // Submission.submissionTime = submissionDTO.time; + // Submission.onBehalfOf = submissionDTO.group!; + // TODO fix group + submission.content = submissionDTO.content; + + return submission; +} diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts new file mode 100644 index 00000000..cddef566 --- /dev/null +++ b/backend/src/interfaces/teacher-invitation.ts @@ -0,0 +1,25 @@ +import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; +import { ClassDTO, mapToClassDTO } from './class.js'; +import { mapToUserDTO, UserDTO } from './user.js'; + +export interface TeacherInvitationDTO { + sender: string | UserDTO; + receiver: string | UserDTO; + class: string | ClassDTO; +} + +export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { + return { + sender: mapToUserDTO(invitation.sender), + receiver: mapToUserDTO(invitation.receiver), + class: mapToClassDTO(invitation.class), + }; +} + +export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): TeacherInvitationDTO { + return { + sender: invitation.sender.username, + receiver: invitation.receiver.username, + class: invitation.class.classId!, + }; +} diff --git a/backend/src/interfaces/teacher.ts b/backend/src/interfaces/teacher.ts new file mode 100644 index 00000000..4dd6adb4 --- /dev/null +++ b/backend/src/interfaces/teacher.ts @@ -0,0 +1,29 @@ +import { Teacher } from '../entities/users/teacher.entity.js'; + +export interface TeacherDTO { + id: string; + username: string; + firstName: string; + lastName: string; + endpoints?: { + classes: string; + questions: string; + invitations: string; + groups: string; + }; +} + +export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { + return { + id: teacher.username, + username: teacher.username, + firstName: teacher.firstName, + lastName: teacher.lastName, + }; +} + +export function mapToTeacher(TeacherData: TeacherDTO): Teacher { + const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName); + + return teacher; +} diff --git a/backend/src/interfaces/user.ts b/backend/src/interfaces/user.ts new file mode 100644 index 00000000..58f0dd5a --- /dev/null +++ b/backend/src/interfaces/user.ts @@ -0,0 +1,30 @@ +import { User } from '../entities/users/user.entity.js'; + +export interface UserDTO { + id?: string; + username: string; + firstName: string; + lastName: string; + endpoints?: { + self: string; + classes: string; + questions: string; + invitations: string; + }; +} + +export function mapToUserDTO(user: User): UserDTO { + return { + id: user.username, + username: user.username, + firstName: user.firstName, + lastName: user.lastName, + }; +} + +export function mapToUser(userData: UserDTO, userInstance: T): T { + userInstance.username = userData.username; + userInstance.firstName = userData.firstName; + userInstance.lastName = userData.lastName; + return userInstance; +} diff --git a/backend/src/routes/assignment.ts b/backend/src/routes/assignment.ts deleted file mode 100644 index 4ae5756d..00000000 --- a/backend/src/routes/assignment.ts +++ /dev/null @@ -1,45 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - assignments: ['0', '1'], - }); -}); - -// Information about an assignment with id 'id' -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - title: 'Dit is een test assignment', - description: 'Een korte beschrijving', - groups: ['0'], - learningPath: '0', - class: '0', - links: { - self: `${req.baseUrl}/${req.params.id}`, - submissions: `${req.baseUrl}/${req.params.id}`, - }, - }); -}); - -router.get('/:id/submissions', (req, res) => { - res.json({ - submissions: ['0'], - }); -}); - -router.get('/:id/groups', (req, res) => { - res.json({ - groups: ['0'], - }); -}); - -router.get('/:id/questions', (req, res) => { - res.json({ - questions: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts new file mode 100644 index 00000000..a733d093 --- /dev/null +++ b/backend/src/routes/assignments.ts @@ -0,0 +1,30 @@ +import express from 'express'; +import { + createAssignmentHandler, + getAllAssignmentsHandler, + getAssignmentHandler, + getAssignmentsSubmissionsHandler, +} from '../controllers/assignments.js'; +import groupRouter from './groups.js'; + +const router = express.Router({ mergeParams: true }); + +// Root endpoint used to search objects +router.get('/', getAllAssignmentsHandler); + +router.post('/', createAssignmentHandler); + +// Information about an assignment with id 'id' +router.get('/:id', getAssignmentHandler); + +router.get('/:id/submissions', getAssignmentsSubmissionsHandler); + +router.get('/:id/questions', (req, res) => { + res.json({ + questions: ['0'], + }); +}); + +router.use('/:assignmentid/groups', groupRouter); + +export default router; diff --git a/backend/src/routes/class.ts b/backend/src/routes/class.ts deleted file mode 100644 index 6f8f324e..00000000 --- a/backend/src/routes/class.ts +++ /dev/null @@ -1,46 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - classes: ['0', '1'], - }); -}); - -// Information about an class with id 'id' -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - displayName: 'Klas 4B', - teachers: ['0'], - students: ['0'], - joinRequests: ['0'], - links: { - 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`, - }, - }); -}); - -router.get('/:id/invitations', (req, res) => { - res.json({ - invitations: ['0'], - }); -}); - -router.get('/:id/assignments', (req, res) => { - res.json({ - assignments: ['0'], - }); -}); - -router.get('/:id/students', (req, res) => { - res.json({ - students: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts new file mode 100644 index 00000000..e0972988 --- /dev/null +++ b/backend/src/routes/classes.ts @@ -0,0 +1,27 @@ +import express from 'express'; +import { + createClassHandler, + getAllClassesHandler, + getClassHandler, + getClassStudentsHandler, + getTeacherInvitationsHandler, +} from '../controllers/classes.js'; +import assignmentRouter from './assignments.js'; + +const router = express.Router(); + +// Root endpoint used to search objects +router.get('/', getAllClassesHandler); + +router.post('/', createClassHandler); + +// Information about an class with id 'id' +router.get('/:id', getClassHandler); + +router.get('/:id/teacher-invitations', getTeacherInvitationsHandler); + +router.get('/:id/students', getClassStudentsHandler); + +router.use('/:classid/assignments', assignmentRouter); + +export default router; diff --git a/backend/src/routes/group.ts b/backend/src/routes/group.ts deleted file mode 100644 index 303f5215..00000000 --- a/backend/src/routes/group.ts +++ /dev/null @@ -1,31 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - groups: ['0', '1'], - }); -}); - -// Information about a group (members, ... [TODO DOC]) -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - assignment: '0', - students: ['0'], - submissions: ['0'], - // Reference to other endpoint - // Should be less hardcoded - questions: `/group/${req.params.id}/question`, - }); -}); - -// The list of questions a group has made -router.get('/:id/question', (req, res) => { - res.json({ - questions: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts new file mode 100644 index 00000000..0c9692b0 --- /dev/null +++ b/backend/src/routes/groups.ts @@ -0,0 +1,23 @@ +import express from 'express'; +import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js'; + +const router = express.Router({ mergeParams: true }); + +// Root endpoint used to search objects +router.get('/', getAllGroupsHandler); + +router.post('/', createGroupHandler); + +// Information about a group (members, ... [TODO DOC]) +router.get('/:groupid', getGroupHandler); + +router.get('/:groupid', getGroupSubmissionsHandler); + +// The list of questions a group has made +router.get('/:id/questions', (req, res) => { + res.json({ + questions: ['0'], + }); +}); + +export default router; diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index b731fe69..7532765b 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -1,6 +1,9 @@ import express from 'express'; import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js'; +import submissionRoutes from './submissions.js'; +import questionRoutes from './questions.js'; + const router = express.Router(); // DWENGO learning objects @@ -21,6 +24,10 @@ router.get('/', getAllLearningObjects); // Example: http://localhost:3000/learningObject/un_ai7 router.get('/:hruid', getLearningObject); +router.use('/:hruid/submissions', submissionRoutes); + +router.use('/:hruid/:version/questions', questionRoutes); + // Parameter: hruid of learning object // Query: language, version (optional) // Route to fetch the HTML rendering of one learning object based on its hruid. diff --git a/backend/src/routes/question.ts b/backend/src/routes/question.ts deleted file mode 100644 index 2e5db624..00000000 --- a/backend/src/routes/question.ts +++ /dev/null @@ -1,33 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - questions: ['0', '1'], - }); -}); - -// Information about an question with id 'id' -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - student: '0', - group: '0', - time: new Date(2025, 1, 1), - content: 'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????', - learningObject: '0', - links: { - self: `${req.baseUrl}/${req.params.id}`, - answers: `${req.baseUrl}/${req.params.id}/answers`, - }, - }); -}); - -router.get('/:id/answers', (req, res) => { - res.json({ - answers: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts new file mode 100644 index 00000000..31a71f3b --- /dev/null +++ b/backend/src/routes/questions.ts @@ -0,0 +1,25 @@ +import express from 'express'; +import { + createQuestionHandler, + deleteQuestionHandler, + getAllQuestionsHandler, + getQuestionAnswersHandler, + getQuestionHandler, +} from '../controllers/questions.js'; +const router = express.Router({ mergeParams: true }); + +// Query language + +// Root endpoint used to search objects +router.get('/', getAllQuestionsHandler); + +router.post('/', createQuestionHandler); + +router.delete('/:seq', deleteQuestionHandler); + +// Information about a question with id +router.get('/:seq', getQuestionHandler); + +router.get('/answers/:seq', getQuestionAnswersHandler); + +export default router; diff --git a/backend/src/routes/student.ts b/backend/src/routes/student.ts deleted file mode 100644 index 9cb0cdee..00000000 --- a/backend/src/routes/student.ts +++ /dev/null @@ -1,55 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - students: ['0', '1'], - }); -}); - -// Information about a student's profile -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - firstName: 'Jimmy', - lastName: 'Faster', - username: 'JimmyFaster2', - endpoints: { - classes: `/student/${req.params.id}/classes`, - questions: `/student/${req.params.id}/submissions`, - invitations: `/student/${req.params.id}/assignments`, - groups: `/student/${req.params.id}/groups`, - }, - }); -}); - -// The list of classes a student is in -router.get('/:id/classes', (req, res) => { - res.json({ - classes: ['0'], - }); -}); - -// The list of submissions a student has made -router.get('/:id/submissions', (req, res) => { - res.json({ - submissions: ['0'], - }); -}); - -// The list of assignments a student has -router.get('/:id/assignments', (req, res) => { - res.json({ - assignments: ['0'], - }); -}); - -// The list of groups a student is in -router.get('/:id/groups', (req, res) => { - res.json({ - groups: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts new file mode 100644 index 00000000..7ed7a666 --- /dev/null +++ b/backend/src/routes/students.ts @@ -0,0 +1,46 @@ +import express from 'express'; +import { + createStudentHandler, + deleteStudentHandler, + getAllStudentsHandler, + getStudentAssignmentsHandler, + getStudentClassesHandler, + getStudentGroupsHandler, + getStudentHandler, + getStudentSubmissionsHandler, +} from '../controllers/students.js'; +import { getStudentGroups } from '../services/students.js'; +const router = express.Router(); + +// Root endpoint used to search objects +router.get('/', getAllStudentsHandler); + +router.post('/', createStudentHandler); + +router.delete('/', deleteStudentHandler); + +router.delete('/:username', deleteStudentHandler); + +// Information about a student's profile +router.get('/:username', getStudentHandler); + +// The list of classes a student is in +router.get('/:id/classes', getStudentClassesHandler); + +// The list of submissions a student has made +router.get('/:id/submissions', getStudentSubmissionsHandler); + +// The list of assignments a student has +router.get('/:id/assignments', getStudentAssignmentsHandler); + +// The list of groups a student is in +router.get('/:id/groups', getStudentGroupsHandler); + +// A list of questions a user has created +router.get('/:id/questions', (req, res) => { + res.json({ + questions: ['0'], + }); +}); + +export default router; diff --git a/backend/src/routes/submission.ts b/backend/src/routes/submission.ts deleted file mode 100644 index cb4d3e85..00000000 --- a/backend/src/routes/submission.ts +++ /dev/null @@ -1,23 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - submissions: ['0', '1'], - }); -}); - -// Information about an submission with id 'id' -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - student: '0', - group: '0', - time: new Date(2025, 1, 1), - content: 'Wortel 2 is rationeel', - learningObject: '0', - }); -}); - -export default router; diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts new file mode 100644 index 00000000..4db93027 --- /dev/null +++ b/backend/src/routes/submissions.ts @@ -0,0 +1,19 @@ +import express from 'express'; +import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler } from '../controllers/submissions.js'; +const router = express.Router({ mergeParams: true }); + +// Root endpoint used to search objects +router.get('/', (req, res) => { + res.json({ + submissions: ['0', '1'], + }); +}); + +router.post('/:id', createSubmissionHandler); + +// Information about an submission with id 'id' +router.get('/:id', getSubmissionHandler); + +router.delete('/:id', deleteSubmissionHandler); + +export default router; diff --git a/backend/src/routes/teacher.ts b/backend/src/routes/teacher.ts deleted file mode 100644 index a7c60bc9..00000000 --- a/backend/src/routes/teacher.ts +++ /dev/null @@ -1,48 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -// Root endpoint used to search objects -router.get('/', (req, res) => { - res.json({ - teachers: ['0', '1'], - }); -}); - -// Information about a teacher -router.get('/:id', (req, res) => { - res.json({ - id: req.params.id, - firstName: 'John', - lastName: 'Doe', - username: 'JohnDoe1', - links: { - self: `${req.baseUrl}/${req.params.id}`, - classes: `${req.baseUrl}/${req.params.id}/classes`, - questions: `${req.baseUrl}/${req.params.id}/questions`, - invitations: `${req.baseUrl}/${req.params.id}/invitations`, - }, - }); -}); - -// The questions students asked a teacher -router.get('/:id/questions', (req, res) => { - res.json({ - questions: ['0'], - }); -}); - -// Invitations to other classes a teacher received -router.get('/:id/invitations', (req, res) => { - res.json({ - invitations: ['0'], - }); -}); - -// A list with ids of classes a teacher is in -router.get('/:id/classes', (req, res) => { - res.json({ - classes: ['0'], - }); -}); - -export default router; diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts new file mode 100644 index 00000000..c04e1575 --- /dev/null +++ b/backend/src/routes/teachers.ts @@ -0,0 +1,37 @@ +import express from 'express'; +import { + createTeacherHandler, + deleteTeacherHandler, + getAllTeachersHandler, + getTeacherClassHandler, + getTeacherHandler, + getTeacherQuestionHandler, + getTeacherStudentHandler, +} from '../controllers/teachers.js'; +const router = express.Router(); + +// Root endpoint used to search objects +router.get('/', getAllTeachersHandler); + +router.post('/', createTeacherHandler); + +router.delete('/', deleteTeacherHandler); + +router.get('/:username', getTeacherHandler); + +router.delete('/:username', deleteTeacherHandler); + +router.get('/:username/classes', getTeacherClassHandler); + +router.get('/:username/students', getTeacherStudentHandler); + +router.get('/:username/questions', getTeacherQuestionHandler); + +// Invitations to other classes a teacher received +router.get('/:id/invitations', (req, res) => { + res.json({ + invitations: ['0'], + }); +}); + +export default router; diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts new file mode 100644 index 00000000..be121810 --- /dev/null +++ b/backend/src/services/assignments.ts @@ -0,0 +1,85 @@ +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'; + +export async function getAllAssignments(classid: string, full: boolean): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return []; + } + + const assignmentRepository = getAssignmentRepository(); + const assignments = await assignmentRepository.findAllAssignmentsInClass(cls); + + if (full) { + return assignments.map(mapToAssignmentDTO); + } + + return assignments.map(mapToAssignmentDTOId); +} + +export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return null; + } + + const assignment = mapToAssignment(assignmentData, cls); + const assignmentRepository = getAssignmentRepository(); + + try { + const newAssignment = assignmentRepository.create(assignment); + await assignmentRepository.save(newAssignment); + + return newAssignment; + } catch (e) { + return null; + } +} + +export async function getAssignment(classid: string, id: number): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return null; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, id); + + if (!assignment) { + return null; + } + + return mapToAssignmentDTO(assignment); +} + +export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return []; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return []; + } + + const groupRepository = getGroupRepository(); + const groups = await groupRepository.findAllGroupsForAssignment(assignment); + + const submissionRepository = getSubmissionRepository(); + const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); + + return submissions.map(mapToSubmissionDTO); +} diff --git a/backend/src/services/class.ts b/backend/src/services/class.ts new file mode 100644 index 00000000..117bffec --- /dev/null +++ b/backend/src/services/class.ts @@ -0,0 +1,99 @@ +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'; +import { getLogger } from '../logging/initalize'; + +const logger = getLogger(); + +export async function getAllClasses(full: boolean): Promise { + const classRepository = getClassRepository(); + const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); + + if (!classes) { + return []; + } + + if (full) { + return classes.map(mapToClassDTO); + } + return classes.map((cls) => cls.classId!); +} + +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 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 classRepository = getClassRepository(); + + try { + const newClass = classRepository.create({ + displayName: classData.displayName, + teachers: teachers, + students: students, + }); + await classRepository.save(newClass); + + return newClass; + } catch (e) { + logger.error(e); + return null; + } +} + +export async function getClass(classId: string): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return null; + } + + return mapToClassDTO(cls); +} + +async function fetchClassStudents(classId: string): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return []; + } + + return cls.students.map(mapToStudentDTO); +} + +export async function getClassStudents(classId: string): Promise { + return await fetchClassStudents(classId); +} + +export async function getClassStudentsIds(classId: string): Promise { + const students: StudentDTO[] = await fetchClassStudents(classId); + return students.map((student) => student.username); +} + +export async function getClassTeacherInvitations(classId: string, full: boolean): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return []; + } + + const teacherInvitationRepository = getTeacherInvitationRepository(); + const invitations = await teacherInvitationRepository.findAllInvitationsForClass(cls); + + if (full) { + return invitations.map(mapToTeacherInvitationDTO); + } + + return invitations.map(mapToTeacherInvitationDTOIds); +} diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts new file mode 100644 index 00000000..91091703 --- /dev/null +++ b/backend/src/services/groups.ts @@ -0,0 +1,132 @@ +import { GroupRepository } from '../data/assignments/group-repository.js'; +import { + getAssignmentRepository, + getClassRepository, + getGroupRepository, + getStudentRepository, + getSubmissionRepository, +} 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'; + +export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return null; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return null; + } + + const groupRepository = getGroupRepository(); + const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber); + + if (!group) { + return null; + } + + if (full) { + return mapToGroupDTO(group); + } + + return mapToGroupDTOId(group); +} + +export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise { + 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); + + console.log(members); + + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classid); + + if (!cls) { + return null; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return null; + } + + const groupRepository = getGroupRepository(); + try { + const newGroup = groupRepository.create({ + assignment: assignment, + members: members, + }); + await groupRepository.save(newGroup); + + return newGroup; + } catch (e) { + console.log(e); + return null; + } +} + +export async function getAllGroups(classId: string, assignmentNumber: number, full: boolean): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return []; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return []; + } + + const groupRepository = getGroupRepository(); + const groups = await groupRepository.findAllGroupsForAssignment(assignment); + + if (full) { + console.log('full'); + console.log(groups); + return groups.map(mapToGroupDTO); + } + + return groups.map(mapToGroupDTOId); +} + +export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + return []; + } + + const assignmentRepository = getAssignmentRepository(); + const assignment = await assignmentRepository.findByClassAndId(cls, assignmentNumber); + + if (!assignment) { + return []; + } + + const groupRepository = getGroupRepository(); + const group = await groupRepository.findByAssignmentAndGroupNumber(assignment, groupNumber); + + if (!group) { + return []; + } + + const submissionRepository = getSubmissionRepository(); + const submissions = await submissionRepository.findAllSubmissionsForGroup(group); + + return submissions.map(mapToSubmissionDTO); +} diff --git a/backend/src/services/learning-objects.ts b/backend/src/services/learning-objects.ts new file mode 100644 index 00000000..fb579471 --- /dev/null +++ b/backend/src/services/learning-objects.ts @@ -0,0 +1,90 @@ +import { DWENGO_API_BASE } from '../config.js'; +import { fetchWithLogging } from '../util/api-helper.js'; +import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; + +function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { + return { + key: data.hruid, // Hruid learningObject (not path) + _id: data._id, + uuid: data.uuid, + version: data.version, + title: data.title, + htmlUrl, // Url to fetch html content + language: data.language, + difficulty: data.difficulty, + estimatedTime: data.estimated_time, + available: data.available, + teacherExclusive: data.teacher_exclusive, + educationalGoals: data.educational_goals, // List with learningObjects + keywords: data.keywords, // For search + description: data.description, // For search (not an actual description) + targetAges: data.target_ages, + contentType: data.content_type, // Markdown, image, audio, etc. + contentLocation: data.content_location, // If content type extern + skosConcepts: data.skos_concepts, + returnValue: data.return_value, // Callback response information + }; +} + +/** + * Fetches a single learning object by its HRUID + */ +export async function getLearningObjectById(hruid: string, language: string): Promise { + const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; + const metadata = await fetchWithLogging( + metadataUrl, + `Metadata for Learning Object HRUID "${hruid}" (language ${language})` + ); + + if (!metadata) { + console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + return null; + } + + const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw?hruid=${hruid}&language=${language}`; + return filterData(metadata, htmlUrl); +} + +/** + * Generic function to fetch learning objects (full data or just HRUIDs) + */ +async function fetchLearningObjects(hruid: string, full: boolean, language: string): Promise { + try { + 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.`); + return []; + } + + const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes; + + if (!full) { + return nodes.map((node) => node.learningobject_hruid); + } + + return await Promise.all(nodes.map(async (node) => getLearningObjectById(node.learningobject_hruid, language))).then((objects) => + objects.filter((obj): obj is FilteredLearningObject => obj !== null) + ); + } catch (error) { + console.error('❌ Error fetching learning objects:', error); + return []; + } +} + +/** + * Fetch full learning object data (metadata) + */ +export async function getLearningObjectsFromPath(hruid: string, language: string): Promise { + return (await fetchLearningObjects(hruid, true, language)) as FilteredLearningObject[]; +} + +/** + * Fetch only learning object HRUIDs + */ +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/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index 37e68c07..dfee329d 100644 --- a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts @@ -1,5 +1,5 @@ import { DWENGO_API_BASE } from '../../config.js'; -import { fetchWithLogging } from '../../util/apiHelper.js'; +import { fetchWithLogging } from '../../util/api-helper.js'; import { FilteredLearningObject, LearningObjectIdentifier, diff --git a/backend/src/services/learning-paths/database-learning-path-provider.ts b/backend/src/services/learning-paths/database-learning-path-provider.ts index fa8f42c6..68986885 100644 --- a/backend/src/services/learning-paths/database-learning-path-provider.ts +++ b/backend/src/services/learning-paths/database-learning-path-provider.ts @@ -29,7 +29,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise it === null)) { + if (Array.from(nullableNodesToLearningObjects.values()).some((it) => it === null)) { throw new Error('At least one of the learning objects on this path could not be found.'); } return nullableNodesToLearningObjects as Map; @@ -41,15 +41,9 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise { const nodesToLearningObjects: Map = await getLearningObjectsForNodes(learningPath.nodes); - const targetAges = nodesToLearningObjects - .values() - .flatMap((it) => it.targetAges || []) - .toArray(); + const targetAges = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.targetAges || []); - const keywords = nodesToLearningObjects - .values() - .flatMap((it) => it.keywords || []) - .toArray(); + const keywords = Array.from(nodesToLearningObjects.values()).flatMap((it) => it.keywords || []); const image = learningPath.image ? learningPath.image.toString('base64') : undefined; @@ -83,27 +77,24 @@ async function convertNodes( nodesToLearningObjects: Map, personalizedFor?: PersonalizationTarget ): Promise { - const nodesPromise = nodesToLearningObjects - .entries() - .map(async (entry) => { - const [node, learningObject] = entry; - const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null; - return { - _id: learningObject.uuid, - language: learningObject.language, - start_node: node.startNode, - created_at: node.createdAt.toISOString(), - updatedAt: node.updatedAt.toISOString(), - learningobject_hruid: node.learningObjectHruid, - version: learningObject.version, - transitions: node.transitions - .filter( - (trans) => !personalizedFor || isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // If we want a personalized learning path, remove all transitions that aren't possible. - ) - .map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition - }; - }) - .toArray(); + const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => { + const [node, learningObject] = entry; + const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null; + return { + _id: learningObject.uuid, + language: learningObject.language, + start_node: node.startNode, + created_at: node.createdAt.toISOString(), + updatedAt: node.updatedAt.toISOString(), + learningobject_hruid: node.learningObjectHruid, + version: learningObject.version, + transitions: node.transitions + .filter( + (trans) => !personalizedFor || isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // If we want a personalized learning path, remove all transitions that aren't possible. + ) + .map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)), // Then convert all the transition + }; + }); return await Promise.all(nodesPromise); } diff --git a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts index 2b1d17a6..a6093bb4 100644 --- a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts @@ -1,4 +1,4 @@ -import { fetchWithLogging } from '../../util/apiHelper.js'; +import { fetchWithLogging } from '../../util/api-helper.js'; import { DWENGO_API_BASE } from '../../config.js'; import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js'; import { LearningPathProvider } from './learning-path-provider.js'; diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts new file mode 100644 index 00000000..ee003bcd --- /dev/null +++ b/backend/src/services/questions.ts @@ -0,0 +1,107 @@ +import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; +import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; +import { Question } from '../entities/questions/question.entity.js'; +import { Answer } from '../entities/questions/answer.entity.js'; +import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js'; +import { QuestionRepository } from '../data/questions/question-repository.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; +import { mapToUser } from '../interfaces/user.js'; +import { Student } from '../entities/users/student.entity.js'; +import { mapToStudent } from '../interfaces/student.js'; + +export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { + const questionRepository: QuestionRepository = getQuestionRepository(); + const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); + + if (!questions) { + return []; + } + + const questionsDTO: QuestionDTO[] = questions.map(mapToQuestionDTO); + + if (full) { + return questionsDTO; + } + + return questionsDTO.map(mapToQuestionId); +} + +async function fetchQuestion(questionId: QuestionId): Promise { + const questionRepository = getQuestionRepository(); + + return await questionRepository.findOne({ + learningObjectHruid: questionId.learningObjectIdentifier.hruid, + learningObjectLanguage: questionId.learningObjectIdentifier.language, + learningObjectVersion: questionId.learningObjectIdentifier.version, + sequenceNumber: questionId.sequenceNumber, + }); +} + +export async function getQuestion(questionId: QuestionId): Promise { + const question = await fetchQuestion(questionId); + + if (!question) { + return null; + } + + return mapToQuestionDTO(question); +} + +export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) { + const answerRepository = getAnswerRepository(); + const question = await fetchQuestion(questionId); + + if (!question) { + return []; + } + + const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question); + + if (!answers) { + return []; + } + + const answersDTO = answers.map(mapToAnswerDTO); + + if (full) { + return answersDTO; + } + + return answersDTO.map(mapToAnswerId); +} + +export async function createQuestion(questionDTO: QuestionDTO) { + const questionRepository = getQuestionRepository(); + + const author = mapToStudent(questionDTO.author); + + try { + await questionRepository.createQuestion({ + loId: questionDTO.learningObjectIdentifier, + author, + content: questionDTO.content, + }); + } catch (e) { + return null; + } + + return questionDTO; +} + +export async function deleteQuestion(questionId: QuestionId) { + const questionRepository = getQuestionRepository(); + + const question = await fetchQuestion(questionId); + + if (!question) { + return null; + } + + try { + await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(questionId.learningObjectIdentifier, questionId.sequenceNumber); + } catch (e) { + return null; + } + + return question; +} diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts new file mode 100644 index 00000000..5099a18d --- /dev/null +++ b/backend/src/services/students.ts @@ -0,0 +1,126 @@ +import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; +import { Class } from '../entities/classes/class.entity.js'; +import { Student } from '../entities/users/student.entity.js'; +import { AssignmentDTO } from '../interfaces/assignment.js'; +import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; +import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; +import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; +import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; +import { getAllAssignments } from './assignments.js'; +import { UserService } from './users.js'; + +export async function getAllStudents(): Promise { + const studentRepository = getStudentRepository(); + const users = await studentRepository.findAll(); + return users.map(mapToStudentDTO); +} + +export async function getAllStudentIds(): Promise { + const users = await getAllStudents(); + return users.map((user) => user.username); +} + +export async function getStudent(username: string): Promise { + const studentRepository = getStudentRepository(); + const user = await studentRepository.findByUsername(username); + return user ? mapToStudentDTO(user) : null; +} + +export async function createStudent(userData: StudentDTO): Promise { + const studentRepository = getStudentRepository(); + + try { + const newStudent = studentRepository.create(mapToStudent(userData)); + await studentRepository.save(newStudent); + + return mapToStudentDTO(newStudent); + } catch (e) { + console.log(e); + return null; + } +} + +export async function deleteStudent(username: string): Promise { + const studentRepository = getStudentRepository(); + + const user = await studentRepository.findByUsername(username); + + if (!user) { + return null; + } + + try { + await studentRepository.deleteByUsername(username); + + return mapToStudentDTO(user); + } catch (e) { + console.log(e); + return null; + } +} + +export async function getStudentClasses(username: string, full: boolean): Promise { + const studentRepository = getStudentRepository(); + const student = await studentRepository.findByUsername(username); + + if (!student) { + return []; + } + + const classRepository = getClassRepository(); + const classes = await classRepository.findByStudent(student); + + if (full) { + return classes.map(mapToClassDTO); + } + + return classes.map((cls) => cls.classId!); +} + +export async function getStudentAssignments(username: string, full: boolean): Promise { + const studentRepository = getStudentRepository(); + const student = await studentRepository.findByUsername(username); + + if (!student) { + return []; + } + + const classRepository = getClassRepository(); + const classes = await classRepository.findByStudent(student); + + const assignments = (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat(); + + return assignments; +} + +export async function getStudentGroups(username: string, full: boolean): Promise { + const studentRepository = getStudentRepository(); + const student = await studentRepository.findByUsername(username); + + if (!student) { + return []; + } + + const groupRepository = getGroupRepository(); + const groups = await groupRepository.findAllGroupsWithStudent(student); + + if (full) { + return groups.map(mapToGroupDTO); + } + + return groups.map(mapToGroupDTOId); +} + +export async function getStudentSubmissions(username: string): Promise { + const studentRepository = getStudentRepository(); + const student = await studentRepository.findByUsername(username); + + if (!student) { + return []; + } + + const submissionRepository = getSubmissionRepository(); + const submissions = await submissionRepository.findAllSubmissionsForStudent(student); + + return submissions.map(mapToSubmissionDTO); +} diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts new file mode 100644 index 00000000..a8fa96c7 --- /dev/null +++ b/backend/src/services/submissions.ts @@ -0,0 +1,51 @@ +import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; +import { Language } from '../entities/content/language.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; +import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; + +export async function getSubmission( + learningObjectHruid: string, + language: Language, + version: number, + submissionNumber: number +): Promise { + const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); + + const submissionRepository = getSubmissionRepository(); + const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber); + + if (!submission) { + return null; + } + + return mapToSubmissionDTO(submission); +} + +export async function createSubmission(submissionDTO: SubmissionDTO) { + const submissionRepository = getSubmissionRepository(); + const submission = mapToSubmission(submissionDTO); + + try { + const newSubmission = await submissionRepository.create(submission); + await submissionRepository.save(newSubmission); + } catch (e) { + return null; + } + + return submission; +} + +export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) { + const submissionRepository = getSubmissionRepository(); + + const submission = getSubmission(learningObjectHruid, language, version, submissionNumber); + + if (!submission) { + return null; + } + + const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); + await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(loId, submissionNumber); + + return submission; +} diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts new file mode 100644 index 00000000..f4dbedfe --- /dev/null +++ b/backend/src/services/teachers.ts @@ -0,0 +1,129 @@ +import { + getClassRepository, + getLearningObjectRepository, + getQuestionRepository, + getStudentRepository, + getTeacherRepository, +} from '../data/repositories.js'; +import { Teacher } from '../entities/users/teacher.entity.js'; +import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; +import { getClassStudents } from './class.js'; +import { StudentDTO } from '../interfaces/student.js'; +import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; +import { UserService } from './users.js'; +import { mapToUser } from '../interfaces/user.js'; +import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js'; + +export async function getAllTeachers(): Promise { + const teacherRepository = getTeacherRepository(); + const users = await teacherRepository.findAll(); + return users.map(mapToTeacherDTO); +} + +export async function getAllTeacherIds(): Promise { + const users = await getAllTeachers(); + return users.map((user) => user.username); +} + +export async function getTeacher(username: string): Promise { + const teacherRepository = getTeacherRepository(); + const user = await teacherRepository.findByUsername(username); + return user ? mapToTeacherDTO(user) : null; +} + +export async function createTeacher(userData: TeacherDTO): Promise { + const teacherRepository = getTeacherRepository(); + + try { + const newTeacher = teacherRepository.create(mapToTeacher(userData)); + await teacherRepository.save(newTeacher); + + return mapToTeacherDTO(newTeacher); + } catch (e) { + console.log(e); + return null; + } +} + +export async function deleteTeacher(username: string): Promise { + const teacherRepository = getTeacherRepository(); + + const user = await teacherRepository.findByUsername(username); + + if (!user) { + return null; + } + + try { + await teacherRepository.deleteByUsername(username); + + return mapToTeacherDTO(user); + } catch (e) { + console.log(e); + return null; + } +} + +export async function fetchClassesByTeacher(username: string): Promise { + const teacherRepository = getTeacherRepository(); + const teacher = await teacherRepository.findByUsername(username); + if (!teacher) { + return []; + } + + const classRepository = getClassRepository(); + const classes = await classRepository.findByTeacher(teacher); + return classes.map(mapToClassDTO); +} + +export async function getClassesByTeacher(username: string): Promise { + return await fetchClassesByTeacher(username); +} + +export async function getClassIdsByTeacher(username: string): Promise { + const classes = await fetchClassesByTeacher(username); + return classes.map((cls) => cls.id); +} + +export async function fetchStudentsByTeacher(username: string) { + const classes = await getClassIdsByTeacher(username); + + return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); +} + +export async function getStudentsByTeacher(username: string): Promise { + return await fetchStudentsByTeacher(username); +} + +export async function getStudentIdsByTeacher(username: string): Promise { + const students = await fetchStudentsByTeacher(username); + return students.map((student) => student.username); +} + +export async function fetchTeacherQuestions(username: string): Promise { + const teacherRepository = getTeacherRepository(); + const teacher = await teacherRepository.findByUsername(username); + if (!teacher) { + throw new Error(`Teacher with username '${username}' not found.`); + } + + // Find all learning objects that this teacher manages + const learningObjectRepository = getLearningObjectRepository(); + const learningObjects = await learningObjectRepository.findAllByTeacher(teacher); + + // Fetch all questions related to these learning objects + const questionRepository = getQuestionRepository(); + const questions = await questionRepository.findAllByLearningObjects(learningObjects); + + return questions.map(mapToQuestionDTO); +} + +export async function getQuestionsByTeacher(username: string): Promise { + return await fetchTeacherQuestions(username); +} + +export async function getQuestionIdsByTeacher(username: string): Promise { + const questions = await fetchTeacherQuestions(username); + + return questions.map(mapToQuestionId); +} diff --git a/backend/src/services/users.ts b/backend/src/services/users.ts new file mode 100644 index 00000000..65ed5d4c --- /dev/null +++ b/backend/src/services/users.ts @@ -0,0 +1,41 @@ +import { UserRepository } from '../data/users/user-repository.js'; +import { UserDTO, mapToUser, mapToUserDTO } from '../interfaces/user.js'; +import { User } from '../entities/users/user.entity.js'; + +export class UserService { + protected repository: UserRepository; + + constructor(repository: UserRepository) { + this.repository = repository; + } + + async getAllUsers(): Promise { + const users = await this.repository.findAll(); + return users.map(mapToUserDTO); + } + + async getAllUserIds(): Promise { + const users = await this.getAllUsers(); + return users.map((user) => user.username); + } + + async getUserByUsername(username: string): Promise { + const user = await this.repository.findByUsername(username); + return user ? mapToUserDTO(user) : null; + } + + async createUser(userData: UserDTO, UserClass: new () => T): Promise { + const newUser = mapToUser(userData, new UserClass()); + await this.repository.save(newUser); + return newUser; + } + + async deleteUser(username: string): Promise { + const user = await this.getUserByUsername(username); + if (!user) { + return null; + } + await this.repository.deleteByUsername(username); + return mapToUserDTO(user); + } +} diff --git a/backend/src/util/apiHelper.ts b/backend/src/util/api-helper.ts similarity index 100% rename from backend/src/util/apiHelper.ts rename to backend/src/util/api-helper.ts diff --git a/backend/src/util/translationHelper.ts b/backend/src/util/translation-helper.ts similarity index 100% rename from backend/src/util/translationHelper.ts rename to backend/src/util/translation-helper.ts diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index 8712a710..cd212b77 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -32,7 +32,7 @@ describe('SubmissionRepository', () => { }); it('should find the requested submission', async () => { - const id = new LearningObjectIdentifier('id03', Language.English, '1'); + const id = new LearningObjectIdentifier('id03', Language.English, 1); const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1); expect(submission).toBeTruthy(); @@ -40,7 +40,7 @@ describe('SubmissionRepository', () => { }); it('should find the most recent submission for a student', async () => { - const id = new LearningObjectIdentifier('id02', Language.English, '1'); + const id = new LearningObjectIdentifier('id02', Language.English, 1); const student = await studentRepository.findByUsername('Noordkaap'); const submission = await submissionRepository.findMostRecentSubmissionForStudent(id, student!); @@ -49,7 +49,7 @@ describe('SubmissionRepository', () => { }); it('should find the most recent submission for a group', async () => { - const id = new LearningObjectIdentifier('id03', Language.English, '1'); + const id = new LearningObjectIdentifier('id03', Language.English, 1); const class_ = await classRepository.findById('id01'); const assignment = await assignmentRepository.findByClassAndId(class_!, 1); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 1); @@ -60,7 +60,7 @@ describe('SubmissionRepository', () => { }); it('should not find a deleted submission', async () => { - const id = new LearningObjectIdentifier('id01', Language.English, '1'); + const id = new LearningObjectIdentifier('id01', Language.English, 1); await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1); const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1); diff --git a/backend/tests/data/content/attachments.test.ts b/backend/tests/data/content/attachments.test.ts index a8bea88a..94e132a9 100644 --- a/backend/tests/data/content/attachments.test.ts +++ b/backend/tests/data/content/attachments.test.ts @@ -17,11 +17,11 @@ describe('AttachmentRepository', () => { }); it('should return the requested attachment', async () => { - const id = new LearningObjectIdentifier('id02', Language.English, '1'); + const id = new LearningObjectIdentifier('id02', Language.English, 1); const learningObject = await learningObjectRepository.findByIdentifier(id); const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName( - learningObject!, + learningObject!.hruid, Language.English, 'attachment01' ); diff --git a/backend/tests/data/content/learning-objects.test.ts b/backend/tests/data/content/learning-objects.test.ts index 51f9c98e..712f75c9 100644 --- a/backend/tests/data/content/learning-objects.test.ts +++ b/backend/tests/data/content/learning-objects.test.ts @@ -13,8 +13,8 @@ describe('LearningObjectRepository', () => { learningObjectRepository = getLearningObjectRepository(); }); - const id01 = new LearningObjectIdentifier('id01', Language.English, '1'); - const id02 = new LearningObjectIdentifier('test_id', Language.English, '1'); + const id01 = new LearningObjectIdentifier('id01', Language.English, 1); + const id02 = new LearningObjectIdentifier('test_id', Language.English, 1); it('should return the learning object that matches identifier 1', async () => { const learningObject = await learningObjectRepository.findByIdentifier(id01); diff --git a/backend/tests/data/questions/answers.test.ts b/backend/tests/data/questions/answers.test.ts index f15fed6a..bcc62cf6 100644 --- a/backend/tests/data/questions/answers.test.ts +++ b/backend/tests/data/questions/answers.test.ts @@ -20,7 +20,7 @@ describe('AnswerRepository', () => { }); it('should find all answers to a question', async () => { - const id = new LearningObjectIdentifier('id05', Language.English, '1'); + const id = new LearningObjectIdentifier('id05', Language.English, 1); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); const question = questions.filter((it) => it.sequenceNumber == 2)[0]; @@ -35,7 +35,7 @@ describe('AnswerRepository', () => { it('should create an answer to a question', async () => { const teacher = await teacherRepository.findByUsername('FooFighters'); - const id = new LearningObjectIdentifier('id05', Language.English, '1'); + const id = new LearningObjectIdentifier('id05', Language.English, 1); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); const question = questions[0]; @@ -54,7 +54,7 @@ describe('AnswerRepository', () => { }); it('should not find a removed answer', async () => { - const id = new LearningObjectIdentifier('id04', Language.English, '1'); + const id = new LearningObjectIdentifier('id04', Language.English, 1); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); await answerRepository.removeAnswerByQuestionAndSequenceNumber(questions[0], 1); diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index 1a1cb034..7b408df4 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -20,7 +20,7 @@ describe('QuestionRepository', () => { }); it('should return all questions part of the given learning object', async () => { - const id = new LearningObjectIdentifier('id05', Language.English, '1'); + const id = new LearningObjectIdentifier('id05', Language.English, 1); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); expect(questions).toBeTruthy(); @@ -28,7 +28,7 @@ describe('QuestionRepository', () => { }); it('should create new question', async () => { - const id = new LearningObjectIdentifier('id03', Language.English, '1'); + const id = new LearningObjectIdentifier('id03', Language.English, 1); const student = await studentRepository.findByUsername('Noordkaap'); await questionRepository.createQuestion({ loId: id, @@ -42,7 +42,7 @@ describe('QuestionRepository', () => { }); it('should not find removed question', async () => { - const id = new LearningObjectIdentifier('id04', Language.English, '1'); + const id = new LearningObjectIdentifier('id04', Language.English, 1); await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(id, 1); const question = await questionRepository.findAllQuestionsAboutLearningObject(id); diff --git a/backend/tests/test_assets/assignments/submission.testdata.ts b/backend/tests/test_assets/assignments/submission.testdata.ts index 058af70f..95dd65df 100644 --- a/backend/tests/test_assets/assignments/submission.testdata.ts +++ b/backend/tests/test_assets/assignments/submission.testdata.ts @@ -12,7 +12,7 @@ export function makeTestSubmissions( const submission01 = em.create(Submission, { learningObjectHruid: 'id03', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), @@ -23,7 +23,7 @@ export function makeTestSubmissions( const submission02 = em.create(Submission, { learningObjectHruid: 'id03', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), @@ -34,7 +34,7 @@ export function makeTestSubmissions( const submission03 = em.create(Submission, { learningObjectHruid: 'id02', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 1, submitter: students[0], submissionTime: new Date(2025, 2, 20), @@ -44,7 +44,7 @@ export function makeTestSubmissions( const submission04 = em.create(Submission, { learningObjectHruid: 'id02', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 2, submitter: students[0], submissionTime: new Date(2025, 2, 25), @@ -54,7 +54,7 @@ export function makeTestSubmissions( const submission05 = em.create(Submission, { learningObjectHruid: 'id01', learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, submissionNumber: 1, submitter: students[1], submissionTime: new Date(2025, 2, 20), diff --git a/backend/tests/test_assets/content/learning-paths.testdata.ts b/backend/tests/test_assets/content/learning-paths.testdata.ts index d2e65c9e..10de885c 100644 --- a/backend/tests/test_assets/content/learning-paths.testdata.ts +++ b/backend/tests/test_assets/content/learning-paths.testdata.ts @@ -77,7 +77,7 @@ export function makeTestLearningPaths(em: EntityManager>, students: Array): Array { const question01 = em.create(Question, { learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, learningObjectHruid: 'id05', sequenceNumber: 1, author: students[0], @@ -16,7 +16,7 @@ export function makeTestQuestions(em: EntityManager> const question02 = em.create(Question, { learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, learningObjectHruid: 'id05', sequenceNumber: 2, author: students[2], @@ -26,7 +26,7 @@ export function makeTestQuestions(em: EntityManager> const question03 = em.create(Question, { learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, learningObjectHruid: 'id04', sequenceNumber: 1, author: students[0], @@ -36,7 +36,7 @@ export function makeTestQuestions(em: EntityManager> const question04 = em.create(Question, { learningObjectLanguage: Language.English, - learningObjectVersion: '1', + learningObjectVersion: 1, learningObjectHruid: 'id01', sequenceNumber: 1, author: students[1], diff --git a/package-lock.json b/package-lock.json index d76437e5..0844e7b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -599,7 +599,8 @@ }, "node_modules/@colors/colors": { "version": "1.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "engines": { "node": ">=0.1.90" } @@ -717,7 +718,8 @@ }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", @@ -1079,6 +1081,8 @@ }, "node_modules/@jsep-plugin/assignment": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", "license": "MIT", "engines": { "node": ">= 10.16.0" @@ -1089,6 +1093,8 @@ }, "node_modules/@jsep-plugin/regex": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", "license": "MIT", "engines": { "node": ">= 10.16.0" @@ -1100,7 +1106,6 @@ "node_modules/@mikro-orm/cli": { "version": "6.4.9", "dev": true, - "license": "MIT", "dependencies": { "@jercle/yargonaut": "1.1.5", "@mikro-orm/core": "6.4.9", @@ -1119,6 +1124,8 @@ }, "node_modules/@mikro-orm/core": { "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.9.tgz", + "integrity": "sha512-osB2TbvSH4ZL1s62LCBQFAnxPqLycX5fakPHOoztudixqfbVD5QQydeGizJXMMh2zKP6vRCwIJy3MeSuFxPjHg==", "license": "MIT", "dependencies": { "dataloader": "2.2.3", @@ -1138,6 +1145,8 @@ }, "node_modules/@mikro-orm/knex": { "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.9.tgz", + "integrity": "sha512-iGXJfe/TziVOQsWuxMIqkOpurysWzQA6kj3+FDtOkHJAijZhqhjSBnfUVHHY/JzU9o0M0rgLrDVJFry/uEaJEA==", "license": "MIT", "dependencies": { "fs-extra": "11.3.0", @@ -1167,6 +1176,8 @@ }, "node_modules/@mikro-orm/postgresql": { "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-6.4.9.tgz", + "integrity": "sha512-ZdVVFAL/TSbzpEmChGdH0oUpy2KiHLjNIeItZHRQgInn1X9p0qx28VVDR78p8qgRGkQ3LquxGTkvmWI0w7qi3A==", "license": "MIT", "dependencies": { "@mikro-orm/knex": "6.4.9", @@ -1184,6 +1195,8 @@ }, "node_modules/@mikro-orm/reflection": { "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.9.tgz", + "integrity": "sha512-fgY7yLrcZm3J/8dv9reUC4PQo7C2muImU31jmzz1SxmNKPJFDJl7OzcDZlM5NOisXzsWUBrcNdCyuQiWViVc3A==", "license": "MIT", "dependencies": { "globby": "11.1.0", @@ -1198,6 +1211,8 @@ }, "node_modules/@mikro-orm/sqlite": { "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mikro-orm/sqlite/-/sqlite-6.4.9.tgz", + "integrity": "sha512-O7Jy/5DrTWpJI/3qkhRJHl+OcECx1N625LHDODAAauOK3+MJB/bj80TrvQhe6d/CHZMmvxZ7m2GzaL1NulKxRw==", "license": "MIT", "dependencies": { "@mikro-orm/knex": "6.4.9", @@ -1212,12 +1227,133 @@ "@mikro-orm/core": "^6.0.0" } }, - "node_modules/@napi-rs/snappy-linux-x64-gnu": { + "node_modules/@napi-rs/snappy-android-arm-eabi": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.2.2.tgz", + "integrity": "sha512-H7DuVkPCK5BlAr1NfSU8bDEN7gYs+R78pSHhDng83QxRnCLmVIZk33ymmIwurmoA1HrdTxbkbuNl+lMvNqnytw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-android-arm64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm64/-/snappy-android-arm64-7.2.2.tgz", + "integrity": "sha512-2R/A3qok+nGtpVK8oUMcrIi5OMDckGYNoBLFyli3zp8w6IArPRfg1yOfVUcHvpUDTo9T7LOS1fXgMOoC796eQw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-arm64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.2.2.tgz", + "integrity": "sha512-USgArHbfrmdbuq33bD5ssbkPIoT7YCXCRLmZpDS6dMDrx+iM7eD2BecNbOOo7/v1eu6TRmQ0xOzeQ6I/9FIi5g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-x64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-x64/-/snappy-darwin-x64-7.2.2.tgz", + "integrity": "sha512-0APDu8iO5iT0IJKblk2lH0VpWSl9zOZndZKnBYIc+ei1npw2L5QvuErFOTeTdHBtzvUHASB+9bvgaWnQo4PvTQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-freebsd-x64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-freebsd-x64/-/snappy-freebsd-x64-7.2.2.tgz", + "integrity": "sha512-mRTCJsuzy0o/B0Hnp9CwNB5V6cOJ4wedDTWEthsdKHSsQlO7WU9W1yP7H3Qv3Ccp/ZfMyrmG98Ad7u7lG58WXA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm-gnueabihf": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm-gnueabihf/-/snappy-linux-arm-gnueabihf-7.2.2.tgz", + "integrity": "sha512-v1uzm8+6uYjasBPcFkv90VLZ+WhLzr/tnfkZ/iD9mHYiULqkqpRuC8zvc3FZaJy5wLQE9zTDkTJN1IvUcZ+Vcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-gnu": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-gnu/-/snappy-linux-arm64-gnu-7.2.2.tgz", + "integrity": "sha512-LrEMa5pBScs4GXWOn6ZYXfQ72IzoolZw5txqUHVGs8eK4g1HR9HTHhb2oY5ySNaKakG5sOgMsb1rwaEnjhChmQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-musl": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-musl/-/snappy-linux-arm64-musl-7.2.2.tgz", + "integrity": "sha512-3orWZo9hUpGQcB+3aTLW7UFDqNCQfbr0+MvV67x8nMNYj5eAeUtMmUE/HxLznHO4eZ1qSqiTwLbVx05/Socdlw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-x64-gnu": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-gnu/-/snappy-linux-x64-gnu-7.2.2.tgz", + "integrity": "sha512-jZt8Jit/HHDcavt80zxEkDpH+R1Ic0ssiVCoueASzMXa7vwPJeF4ZxZyqUw4qeSy7n8UUExomu8G8ZbP6VKhgw==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1228,10 +1364,11 @@ }, "node_modules/@napi-rs/snappy-linux-x64-musl": { "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-musl/-/snappy-linux-x64-musl-7.2.2.tgz", + "integrity": "sha512-Dh96IXgcZrV39a+Tej/owcd9vr5ihiZ3KRix11rr1v0MWtVb61+H1GXXlz6+Zcx9y8jM1NmOuiIuJwkV4vZ4WA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1240,6 +1377,51 @@ "node": ">= 10" } }, + "node_modules/@napi-rs/snappy-win32-arm64-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-arm64-msvc/-/snappy-win32-arm64-msvc-7.2.2.tgz", + "integrity": "sha512-9No0b3xGbHSWv2wtLEn3MO76Yopn1U2TdemZpCaEgOGccz1V+a/1d16Piz3ofSmnA13HGFz3h9NwZH9EOaIgYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-ia32-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-ia32-msvc/-/snappy-win32-ia32-msvc-7.2.2.tgz", + "integrity": "sha512-QiGe+0G86J74Qz1JcHtBwM3OYdTni1hX1PFyLRo3HhQUSpmi13Bzc1En7APn+6Pvo7gkrcy81dObGLDSxFAkQQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-x64-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-x64-msvc/-/snappy-win32-x64-msvc-7.2.2.tgz", + "integrity": "sha512-a43cyx1nK0daw6BZxVcvDEXxKMFLSBSDTAhsFD0VqSKcC7MGUBMaqyoWUcMiI7LBSz4bxUmxDWKfCYzpEmeb3w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -1336,23 +1518,28 @@ }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1360,23 +1547,28 @@ }, "node_modules/@protobufjs/float": { "version": "1.0.2", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "node_modules/@protobufjs/path": { "version": "1.1.2", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", @@ -1516,8 +1708,9 @@ }, "node_modules/@types/cors": { "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -1580,7 +1773,8 @@ }, "node_modules/@types/jsonwebtoken": { "version": "9.0.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", "dependencies": { "@types/ms": "*", "@types/node": "*" @@ -1592,7 +1786,8 @@ }, "node_modules/@types/ms": { "version": "2.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, "node_modules/@types/node": { "version": "22.13.4", @@ -1611,8 +1806,9 @@ }, "node_modules/@types/response-time": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.8.tgz", + "integrity": "sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==", "dev": true, - "license": "MIT", "dependencies": { "@types/express": "*", "@types/node": "*" @@ -1653,10 +1849,13 @@ }, "node_modules/@types/triple-beam": { "version": "1.3.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, "node_modules/@types/trusted-types": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", "optional": true }, @@ -2444,11 +2643,13 @@ }, "node_modules/async": { "version": "3.2.6", - "license": "MIT" + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, "node_modules/async-exit-hook": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "engines": { "node": ">=0.12.0" } @@ -2459,6 +2660,8 @@ }, "node_modules/axios": { "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2598,7 +2801,8 @@ }, "node_modules/btoa": { "version": "1.2.1", - "license": "(MIT OR Apache-2.0)", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", "bin": { "btoa": "bin/btoa.js" }, @@ -2630,7 +2834,8 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, "node_modules/bundle-name": { "version": "4.1.0", @@ -2932,7 +3137,8 @@ }, "node_modules/color": { "version": "3.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" @@ -2955,7 +3161,8 @@ }, "node_modules/color-string": { "version": "1.9.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -2971,14 +3178,16 @@ }, "node_modules/color/node_modules/color-convert": { "version": "1.9.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { "color-name": "1.1.3" } }, "node_modules/color/node_modules/color-name": { "version": "1.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/colorette": { "version": "2.0.19", @@ -2986,7 +3195,8 @@ }, "node_modules/colorspace": { "version": "1.1.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" @@ -3080,7 +3290,8 @@ }, "node_modules/cors": { "version": "2.8.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -3096,6 +3307,8 @@ }, "node_modules/cross": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cross/-/cross-1.0.0.tgz", + "integrity": "sha512-p6hXbCnjuIB4bhKWFeztQd7VwffgQP9zOBzUoiA8Lvi01RzQY0e7PbPFU/uqVPTM2stY7uCpVck1UTPpxhinMQ==", "license": "BSD" }, "node_modules/cross-env": { @@ -3325,6 +3538,8 @@ }, "node_modules/dompurify": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -3371,7 +3586,8 @@ }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dependencies": { "safe-buffer": "^5.0.1" } @@ -3423,7 +3639,8 @@ }, "node_modules/enabled": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, "node_modules/encodeurl": { "version": "2.0.0", @@ -3990,7 +4207,8 @@ }, "node_modules/express-jwt": { "version": "8.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", + "integrity": "sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==", "dependencies": { "@types/jsonwebtoken": "^9", "express-unless": "^2.1.3", @@ -4002,7 +4220,8 @@ }, "node_modules/express-unless": { "version": "2.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==" }, "node_modules/express/node_modules/debug": { "version": "4.3.6", @@ -4076,7 +4295,8 @@ }, "node_modules/fecha": { "version": "4.2.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, "node_modules/figlet": { "version": "1.8.0", @@ -4196,7 +4416,8 @@ }, "node_modules/fn.name": { "version": "1.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { "version": "1.15.9", @@ -4475,6 +4696,8 @@ }, "node_modules/gift-pegjs": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/gift-pegjs/-/gift-pegjs-1.0.2.tgz", + "integrity": "sha512-S/A2wBDdia2QWKpB5FtASx1gguep1wg5If5glDWJgUMiABICJT7ogArGfsdgozevhBdbdOiHhrykJP86hbgvRw==", "license": "MIT", "dependencies": { "pegjs": "^0.10.x" @@ -4815,7 +5038,8 @@ }, "node_modules/is-arrayish": { "version": "0.3.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "node_modules/is-core-module": { "version": "2.16.1", @@ -4970,6 +5194,8 @@ }, "node_modules/isomorphic-dompurify": { "version": "2.22.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.22.0.tgz", + "integrity": "sha512-A2xsDNST1yB94rErEnwqlzSvGllCJ4e8lDMe1OWBH2hvpfc/2qzgMEiDshTO1HwO+PIDTiYeOc7ZDB7Ds49BOg==", "license": "MIT", "dependencies": { "dompurify": "^3.2.4", @@ -5003,7 +5229,8 @@ }, "node_modules/jose": { "version": "4.15.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -5103,6 +5330,8 @@ }, "node_modules/jsep": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", "engines": { "node": ">= 10.16.0" @@ -5165,6 +5394,8 @@ }, "node_modules/jsonpath-plus": { "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", "license": "MIT", "dependencies": { "@jsep-plugin/assignment": "^1.3.0", @@ -5181,7 +5412,8 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", @@ -5201,7 +5433,8 @@ }, "node_modules/jwa": { "version": "1.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -5210,7 +5443,8 @@ }, "node_modules/jwks-rsa": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", "dependencies": { "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.2", @@ -5225,7 +5459,8 @@ }, "node_modules/jwks-rsa/node_modules/@types/express": { "version": "4.17.21", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -5235,7 +5470,8 @@ }, "node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": { "version": "4.19.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -5245,7 +5481,8 @@ }, "node_modules/jws": { "version": "3.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" @@ -5253,7 +5490,8 @@ }, "node_modules/jwt-decode": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "engines": { "node": ">=18" } @@ -5348,7 +5586,8 @@ }, "node_modules/kuler": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, "node_modules/levn": { "version": "0.4.1", @@ -5363,7 +5602,9 @@ } }, "node_modules/limiter": { - "version": "1.1.5" + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -5385,31 +5626,38 @@ }, "node_modules/lodash.clonedeep": { "version": "4.5.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, "node_modules/lodash.includes": { "version": "4.3.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, "node_modules/lodash.isboolean": { "version": "3.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, "node_modules/lodash.isinteger": { "version": "4.0.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" }, "node_modules/lodash.isnumber": { "version": "3.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -5418,11 +5666,13 @@ }, "node_modules/lodash.once": { "version": "4.1.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/logform": { "version": "2.7.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", @@ -5437,13 +5687,16 @@ }, "node_modules/loki-logger-ts": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/loki-logger-ts/-/loki-logger-ts-1.0.2.tgz", + "integrity": "sha512-SV/B5o+9jaxiThcU5N3LUxCNTx20IgR9xjCjx/ED/pVc/097mqKSRpmvSjvx9ezFcjJlUF7GBkrBBpR6veNp7Q==", "dependencies": { "axios": "^1.4.0" } }, "node_modules/long": { "version": "5.3.1", - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==" }, "node_modules/loupe": { "version": "3.1.3", @@ -5456,7 +5709,8 @@ }, "node_modules/lru-memoizer": { "version": "2.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", "dependencies": { "lodash.clonedeep": "^4.5.0", "lru-cache": "6.0.0" @@ -5464,7 +5718,8 @@ }, "node_modules/lru-memoizer/node_modules/lru-cache": { "version": "6.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { "yallist": "^4.0.0" }, @@ -5474,7 +5729,8 @@ }, "node_modules/lru-memoizer/node_modules/yallist": { "version": "4.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/magic-string": { "version": "0.30.17", @@ -5587,6 +5843,8 @@ }, "node_modules/marked": { "version": "15.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz", + "integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -5653,6 +5911,8 @@ }, "node_modules/mikro-orm": { "version": "6.4.9", + "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.9.tgz", + "integrity": "sha512-XwVrWNT4NNwS6kHIKFNDfvy8L1eWcBBEHeTVzFFYcnb2ummATaLxqeVkNEmKA68jmdtfQdUmWBqGdbcIPwtL2Q==", "license": "MIT", "engines": { "node": ">= 18.12.0" @@ -6183,7 +6443,8 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { "node": ">=0.10.0" } @@ -6200,6 +6461,8 @@ }, "node_modules/oidc-client-ts": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.0.tgz", + "integrity": "sha512-wUvVcG3SXzZDKHxi/VGQGaTUk9qguMKfYh26Y1zOVrQsu1zp85JWx/SjZzKSXK5j3NA1RcasgMoaHe6gt1WNtw==", "license": "Apache-2.0", "dependencies": { "jwt-decode": "^4.0.0" @@ -6220,7 +6483,8 @@ }, "node_modules/on-headers": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "engines": { "node": ">= 0.8" } @@ -6234,7 +6498,8 @@ }, "node_modules/one-time": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", "dependencies": { "fn.name": "1.x.x" } @@ -6440,6 +6705,8 @@ }, "node_modules/pegjs": { "version": "0.10.0", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", + "integrity": "sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow==", "license": "MIT", "bin": { "pegjs": "bin/pegjs" @@ -6455,6 +6722,8 @@ }, "node_modules/pg": { "version": "8.13.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz", + "integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==", "license": "MIT", "dependencies": { "pg-connection-string": "^2.7.0", @@ -6480,6 +6749,8 @@ }, "node_modules/pg-cloudflare": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", "license": "MIT", "optional": true }, @@ -6489,6 +6760,8 @@ }, "node_modules/pg-int8": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", "license": "ISC", "engines": { "node": ">=4.0.0" @@ -6496,6 +6769,8 @@ }, "node_modules/pg-pool": { "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" @@ -6503,6 +6778,8 @@ }, "node_modules/pg-protocol": { "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==", "license": "MIT" }, "node_modules/pg-types": { @@ -6521,6 +6798,8 @@ }, "node_modules/pg-types/node_modules/postgres-array": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "license": "MIT", "engines": { "node": ">=4" @@ -6528,6 +6807,8 @@ }, "node_modules/pg-types/node_modules/postgres-date": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6535,6 +6816,8 @@ }, "node_modules/pg-types/node_modules/postgres-interval": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "license": "MIT", "dependencies": { "xtend": "^4.0.0" @@ -6545,6 +6828,8 @@ }, "node_modules/pg/node_modules/pg-connection-string": { "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", "license": "MIT" }, "node_modules/pgpass": { @@ -6647,6 +6932,8 @@ }, "node_modules/postgres-array": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", "license": "MIT", "engines": { "node": ">=12" @@ -6654,6 +6941,8 @@ }, "node_modules/postgres-bytea": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6769,8 +7058,9 @@ }, "node_modules/protobufjs": { "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, - "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -6982,7 +7272,8 @@ }, "node_modules/response-time": { "version": "2.3.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.3.tgz", + "integrity": "sha512-SsjjOPHl/FfrTQNgmc5oen8Hr1Jxpn6LlHNXxCIFdYMHuK1kMeYMobb9XN3mvxaGQm3dbegqYFMX4+GDORfbWg==", "dependencies": { "depd": "~2.0.0", "on-headers": "~1.0.1" @@ -7170,7 +7461,8 @@ }, "node_modules/safe-stable-stringify": { "version": "2.5.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "engines": { "node": ">=10" } @@ -7417,7 +7709,8 @@ }, "node_modules/simple-swizzle": { "version": "0.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "dependencies": { "is-arrayish": "^0.3.1" } @@ -7453,7 +7746,8 @@ }, "node_modules/snappy": { "version": "7.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-7.2.2.tgz", + "integrity": "sha512-iADMq1kY0v3vJmGTuKcFWSXt15qYUz7wFkArOrsSg0IFfI3nJqIJvK2/ZbEIndg7erIJLtAVX2nSOqPz7DcwbA==", "optional": true, "engines": { "node": ">= 10" @@ -7532,6 +7826,8 @@ }, "node_modules/split2": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "license": "ISC", "engines": { "node": ">= 10.x" @@ -7607,7 +7903,8 @@ }, "node_modules/stack-trace": { "version": "0.0.10", - "license": "MIT", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", "engines": { "node": "*" } @@ -7968,7 +8265,8 @@ }, "node_modules/text-hex": { "version": "1.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, "node_modules/tildify": { "version": "2.0.0", @@ -8072,7 +8370,8 @@ }, "node_modules/triple-beam": { "version": "1.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", "engines": { "node": ">= 14.0.0" } @@ -8335,7 +8634,8 @@ }, "node_modules/url-polyfill": { "version": "1.1.13", - "license": "MIT" + "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.13.tgz", + "integrity": "sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ==" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -8960,7 +9260,8 @@ }, "node_modules/winston": { "version": "3.17.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", @@ -8980,7 +9281,8 @@ }, "node_modules/winston-loki": { "version": "6.1.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.1.3.tgz", + "integrity": "sha512-DjWtJ230xHyYQWr9mZJa93yhwHttn3JEtSYWP8vXZWJOahiQheUhf+88dSIidbGXB3u0oLweV6G1vkL/ouT62Q==", "dependencies": { "async-exit-hook": "2.0.1", "btoa": "^1.2.1", @@ -8994,7 +9296,8 @@ }, "node_modules/winston-transport": { "version": "4.9.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", @@ -9006,7 +9309,8 @@ }, "node_modules/winston/node_modules/is-stream": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "engines": { "node": ">=8" }, @@ -9140,6 +9444,8 @@ }, "node_modules/xtend": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", "engines": { "node": ">=0.4"