diff --git a/backend/Dockerfile b/backend/Dockerfile index a7aaa6b3..d944fd5a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,3 +1,5 @@ +#syntax=docker/dockerfile:1.7-labs + FROM node:22 AS build-stage WORKDIR /app/dwengo @@ -17,7 +19,7 @@ RUN npm install --silent # Root tsconfig.json COPY tsconfig.json tsconfig.build.json ./ -COPY backend ./backend +COPY --exclude=backend/tests/ backend ./backend COPY common ./common COPY docs ./docs diff --git a/backend/package.json b/backend/package.json index b0264ea8..555fb6ac 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "@dwengo-1/backend", - "version": "0.2.0", + "version": "1.0.0", "description": "Backend for Dwengo-1", "private": true, "type": "module", diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index 2ca6d2fc..793a1f45 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -11,8 +11,7 @@ import { import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { requireFields } from './error-helper.js'; import { BadRequestException } from '../exceptions/bad-request-exception.js'; -import { Assignment } from '../entities/assignments/assignment.entity.js'; -import { EntityDTO } from '@mikro-orm/core'; +import { FALLBACK_LANG } from '../config.js'; function getAssignmentParams(req: Request): { classid: string; assignmentNumber: number; full: boolean } { const classid = req.params.classid; @@ -38,14 +37,19 @@ export async function getAllAssignmentsHandler(req: Request, res: Response): Pro export async function createAssignmentHandler(req: Request, res: Response): Promise { const classid = req.params.classid; - const description = req.body.description; - const language = req.body.language; - const learningPath = req.body.learningPath; + const description = req.body.description || ''; + const language = req.body.language || FALLBACK_LANG; + const learningPath = req.body.learningPath || ''; const title = req.body.title; - requireFields({ description, language, learningPath, title }); + requireFields({ title }); - const assignmentData = req.body as AssignmentDTO; + const assignmentData = { + description: description, + language: language, + learningPath: learningPath, + title: title, + } as AssignmentDTO; const assignment = await createAssignment(classid, assignmentData); res.json({ assignment }); @@ -62,7 +66,7 @@ export async function getAssignmentHandler(req: Request, res: Response): Promise export async function putAssignmentHandler(req: Request, res: Response): Promise { const { classid, assignmentNumber } = getAssignmentParams(req); - const assignmentData = req.body as Partial>; + const assignmentData = req.body as Partial; const assignment = await putAssignment(classid, assignmentNumber, assignmentData); res.json({ assignment }); diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 7fdefd2d..a38f50e3 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -1,4 +1,4 @@ -import { Request, Response } from 'express'; +import { Response } from 'express'; import { themes } from '../data/themes.js'; import { FALLBACK_LANG } from '../config.js'; import learningPathService from '../services/learning-paths/learning-path-service.js'; @@ -15,7 +15,7 @@ import { requireFields } from './error-helper.js'; /** * Fetch learning paths based on query parameters. */ -export async function getLearningPaths(req: Request, res: Response): Promise { +export async function getLearningPaths(req: AuthenticatedRequest, res: Response): Promise { const admin = req.query.admin; if (admin) { const paths = await learningPathService.getLearningPathsAdministratedBy(admin as string); @@ -59,6 +59,19 @@ export async function getLearningPaths(req: Request, res: Response): Promise theme.hruids); + + const apiLearningPathResponse = await learningPathService.fetchLearningPaths(hruidList, language as Language, 'All themes', forGroup); + const apiLearningPaths: LearningPath[] = apiLearningPathResponse.data || []; + let allLearningPaths: LearningPath[] = apiLearningPaths; + + if (req.auth) { + const adminUsername = req.auth.username; + const userLearningPaths = (await learningPathService.getLearningPathsAdministratedBy(adminUsername)) || []; + allLearningPaths = apiLearningPaths.concat(userLearningPaths); + } + + res.json(allLearningPaths); + return; } const learningPaths = await learningPathService.fetchLearningPaths( diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts index 6d8ab0bc..4eb15059 100644 --- a/backend/src/controllers/teachers.ts +++ b/backend/src/controllers/teachers.ts @@ -7,6 +7,7 @@ import { getJoinRequestsByClass, getStudentsByTeacher, getTeacher, + getTeacherAssignments, updateClassJoinRequestStatus, } from '../services/teachers.js'; import { requireFields } from './error-helper.js'; @@ -59,6 +60,16 @@ export async function getTeacherClassHandler(req: Request, res: Response): Promi res.json({ classes }); } +export async function getTeacherAssignmentsHandler(req: Request, res: Response): Promise { + const username = req.params.username; + const full = req.query.full === 'true'; + requireFields({ username }); + + const assignments = await getTeacherAssignments(username, full); + + res.json({ assignments }); +} + export async function getTeacherStudentHandler(req: Request, res: Response): Promise { const username = req.params.username; const full = req.query.full === 'true'; diff --git a/backend/src/data/assignments/assignment-repository.ts b/backend/src/data/assignments/assignment-repository.ts index 1c8bb504..2ca13545 100644 --- a/backend/src/data/assignments/assignment-repository.ts +++ b/backend/src/data/assignments/assignment-repository.ts @@ -7,7 +7,7 @@ export class AssignmentRepository extends DwengoEntityRepository { return this.findOne({ within: within, id: id }, { populate: ['groups', 'groups.members'] }); } public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise { - return this.findOne({ within: { classId: withinClass }, id: id }); + return this.findOne({ within: { classId: withinClass }, id: id }, { populate: ['groups', 'groups.members'] }); } public async findAllByResponsibleTeacher(teacherUsername: string): Promise { return this.findAll({ @@ -20,6 +20,7 @@ export class AssignmentRepository extends DwengoEntityRepository { }, }, }, + populate: ['groups', 'groups.members'], }); } public async findAllAssignmentsInClass(within: Class): Promise { diff --git a/backend/src/data/assignments/group-repository.ts b/backend/src/data/assignments/group-repository.ts index f06080f7..2e8ec067 100644 --- a/backend/src/data/assignments/group-repository.ts +++ b/backend/src/data/assignments/group-repository.ts @@ -28,4 +28,9 @@ export class GroupRepository extends DwengoEntityRepository { groupNumber: groupNumber, }); } + public async deleteAllByAssignment(assignment: Assignment): Promise { + return this.deleteAllWhere({ + assignment: assignment, + }); + } } diff --git a/backend/src/data/content/learning-path-repository.ts b/backend/src/data/content/learning-path-repository.ts index 238a7676..87a03748 100644 --- a/backend/src/data/content/learning-path-repository.ts +++ b/backend/src/data/content/learning-path-repository.ts @@ -7,14 +7,20 @@ import { LearningPathTransition } from '../../entities/content/learning-path-tra export class LearningPathRepository extends DwengoEntityRepository { public async findByHruidAndLanguage(hruid: string, language: Language): Promise { - return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions', 'admins'] }); + return this.findOne( + { + hruid: hruid, + language: language, + }, + { populate: ['nodes', 'nodes.transitions', 'admins'] } + ); } /** * Returns all learning paths which have the given language and whose title OR description contains the * query string. * - * @param query The query string we want to seach for in the title or description. + * @param query The query string we want to search for in the title or description. * @param language The language of the learning paths we want to find. */ public async findByQueryStringAndLanguage(query: string, language: Language): Promise { diff --git a/backend/src/data/dwengo-entity-repository.ts b/backend/src/data/dwengo-entity-repository.ts index 1267c726..64a129ce 100644 --- a/backend/src/data/dwengo-entity-repository.ts +++ b/backend/src/data/dwengo-entity-repository.ts @@ -1,12 +1,25 @@ -import { EntityRepository, FilterQuery } from '@mikro-orm/core'; +import { EntityRepository, FilterQuery, SyntaxErrorException } from '@mikro-orm/core'; import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js'; +import { getLogger } from '../logging/initalize.js'; export abstract class DwengoEntityRepository extends EntityRepository { public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise { if (options?.preventOverwrite && (await this.findOne(entity))) { throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); } - await this.getEntityManager().persistAndFlush(entity); + try { + await this.getEntityManager().persistAndFlush(entity); + } catch (e: unknown) { + // Workaround for MikroORM bug: Sometimes, queries are generated with random syntax errors. + // The faulty query is then retried everytime something is persisted. By clearing the entity + // Manager in that case, we make sure that future queries will work. + if (e instanceof SyntaxErrorException) { + getLogger().error('SyntaxErrorException caught => entity manager cleared.'); + this.em.clear(); + } else { + throw e; + } + } } public async deleteWhere(query: FilterQuery): Promise { const toDelete = await this.findOne(query); @@ -16,4 +29,13 @@ export abstract class DwengoEntityRepository extends EntityRep await em.flush(); } } + public async deleteAllWhere(query: FilterQuery): Promise { + const toDelete = await this.find(query); + const em = this.getEntityManager(); + + if (toDelete) { + em.remove(toDelete); + await em.flush(); + } + } } diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index f681eebb..342751f2 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -18,13 +18,9 @@ export class QuestionRepository extends DwengoEntityRepository { content: question.content, timestamp: new Date(), }); - questionEntity.learningObjectHruid = question.loId.hruid; - questionEntity.learningObjectLanguage = question.loId.language; - questionEntity.learningObjectVersion = question.loId.version; - questionEntity.author = question.author; - questionEntity.inGroup = question.inGroup; - questionEntity.content = question.content; - return await this.insert(questionEntity); + // Don't check for overwrite since this is impossible anyway due to autoincrement. + await this.save(questionEntity, { preventOverwrite: false }); + return questionEntity; } public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise { return this.findAll({ diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 44ccfbd3..0b41f2f7 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -6,6 +6,9 @@ import { Group } from '../assignments/group.entity.js'; @Entity({ repository: () => QuestionRepository }) export class Question { + @PrimaryKey({ type: 'integer', autoincrement: true }) + sequenceNumber?: number; + @PrimaryKey({ type: 'string' }) learningObjectHruid!: string; @@ -18,9 +21,6 @@ export class Question { @PrimaryKey({ type: 'number' }) learningObjectVersion = 1; - @PrimaryKey({ type: 'integer', autoincrement: true }) - sequenceNumber?: number; - @ManyToOne({ entity: () => Group }) inGroup!: Group; diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts index 2dc158d2..32c2f5c8 100644 --- a/backend/src/interfaces/assignment.ts +++ b/backend/src/interfaces/assignment.ts @@ -20,7 +20,7 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO { description: assignment.description, learningPath: assignment.learningPathHruid, language: assignment.learningPathLanguage, - deadline: assignment.deadline ?? new Date(), + deadline: assignment.deadline ?? null, groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)), }; } diff --git a/backend/src/middleware/auth/checks/submission-checks.ts b/backend/src/middleware/auth/checks/submission-checks.ts index 893371c2..1b781773 100644 --- a/backend/src/middleware/auth/checks/submission-checks.ts +++ b/backend/src/middleware/auth/checks/submission-checks.ts @@ -7,10 +7,16 @@ import { authorize } from './auth-checks.js'; import { FALLBACK_LANG } from '../../../config.js'; import { mapToUsername } from '../../../interfaces/user.js'; import { AccountType } from '@dwengo-1/common/util/account-types'; +import { fetchClass } from '../../../services/classes.js'; +import { fetchGroup } from '../../../services/groups.js'; +import { requireFields } from '../../../controllers/error-helper.js'; +import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; -export const onlyAllowSubmitter = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username -); +export const onlyAllowSubmitter = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const submittedFor = (req.body as SubmissionDTO).submitter.username; + const submittedBy = auth.username; + return submittedFor === submittedBy; +}); export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const { hruid: lohruid, id: submissionNumber } = req.params; @@ -26,3 +32,17 @@ export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: Authentic return submission.onBehalfOf.members.map(mapToUsername).includes(auth.username); }); + +export const onlyAllowIfHasAccessToSubmissionFromParams = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const { classId, assignmentId, forGroup } = req.query; + + requireFields({ classId, assignmentId, forGroup }); + + if (auth.accountType === AccountType.Teacher) { + const cls = await fetchClass(classId as string); + return cls.teachers.map(mapToUsername).includes(auth.username); + } + + const group = await fetchGroup(classId as string, Number(assignmentId as string), Number(forGroup as string)); + return group.members.map(mapToUsername).includes(auth.username); +}); diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts index 88309ce8..58176b40 100644 --- a/backend/src/routes/submissions.ts +++ b/backend/src/routes/submissions.ts @@ -1,10 +1,14 @@ import express from 'express'; import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js'; -import { onlyAllowIfHasAccessToSubmission, onlyAllowSubmitter } from '../middleware/auth/checks/submission-checks.js'; -import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js'; +import { + onlyAllowIfHasAccessToSubmission, + onlyAllowIfHasAccessToSubmissionFromParams, + onlyAllowSubmitter, +} from '../middleware/auth/checks/submission-checks.js'; +import { studentsOnly } from '../middleware/auth/checks/auth-checks.js'; const router = express.Router({ mergeParams: true }); -router.get('/', adminOnly, getSubmissionsHandler); +router.get('/', onlyAllowIfHasAccessToSubmissionFromParams, getSubmissionsHandler); router.post('/', studentsOnly, onlyAllowSubmitter, createSubmissionHandler); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index cb2405aa..c7280f02 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -4,6 +4,7 @@ import { deleteTeacherHandler, getAllTeachersHandler, getStudentJoinRequestHandler, + getTeacherAssignmentsHandler, getTeacherClassHandler, getTeacherHandler, getTeacherStudentHandler, @@ -28,6 +29,8 @@ router.get('/:username/classes', preventImpersonation, getTeacherClassHandler); router.get('/:username/students', preventImpersonation, getTeacherStudentHandler); +router.get(`/:username/assignments`, getTeacherAssignmentsHandler); + router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler); router.put('/:username/joinRequests/:classId/:studentUsername', onlyAllowTeacherOfClass, updateStudentJoinRequestHandler); diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts index 2379ecfb..afa4c759 100644 --- a/backend/src/services/assignments.ts +++ b/backend/src/services/assignments.ts @@ -14,10 +14,13 @@ import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submissi import { fetchClass } from './classes.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; -import { EntityDTO } from '@mikro-orm/core'; +import { EntityDTO, ForeignKeyConstraintViolationException } from '@mikro-orm/core'; import { putObject } from './service-helper.js'; import { fetchStudents } from './students.js'; import { ServerErrorException } from '../exceptions/server-error-exception.js'; +import { BadRequestException } from '../exceptions/bad-request-exception.js'; +import { ConflictException } from '../exceptions/conflict-exception.js'; +import { PostgreSqlExceptionConverter } from '@mikro-orm/postgresql'; export async function fetchAssignment(classid: string, assignmentNumber: number): Promise { const classRepository = getClassRepository(); @@ -59,7 +62,7 @@ export async function createAssignment(classid: string, assignmentData: Assignme if (assignmentData.groups) { /* - For some reason when trying to add groups, it does not work when using the original assignment variable. + For some reason when trying to add groups, it does not work when using the original assignment variable. The assignment needs to be refetched in order for it to work. */ @@ -93,10 +96,45 @@ export async function getAssignment(classid: string, id: number): Promise>): Promise { +function hasDuplicates(arr: string[]): boolean { + return new Set(arr).size !== arr.length; +} + +export async function putAssignment(classid: string, id: number, assignmentData: Partial): Promise { const assignment = await fetchAssignment(classid, id); - await putObject(assignment, assignmentData, getAssignmentRepository()); + if (assignmentData.groups) { + if (hasDuplicates(assignmentData.groups.flat() as string[])) { + throw new BadRequestException('Student can only be in one group'); + } + + const studentLists = await Promise.all((assignmentData.groups as string[][]).map(async (group) => await fetchStudents(group))); + + try { + const groupRepository = getGroupRepository(); + await groupRepository.deleteAllByAssignment(assignment); + + await Promise.all( + studentLists.map(async (students) => { + const newGroup = groupRepository.create({ + assignment: assignment, + members: students, + }); + await groupRepository.save(newGroup); + }) + ); + } catch (e: unknown) { + if (e instanceof ForeignKeyConstraintViolationException || e instanceof PostgreSqlExceptionConverter) { + throw new ConflictException('Cannot update assigment with questions or submissions'); + } else { + throw e; + } + } + + delete assignmentData.groups; + } + + await putObject(assignment, assignmentData as Partial>, getAssignmentRepository()); return mapToAssignmentDTO(assignment); } @@ -106,7 +144,16 @@ export async function deleteAssignment(classid: string, id: number): Promise !it.done).length, + num_nodes: nodesActuallyOnPath.length, + num_nodes_left: nodesActuallyOnPath.filter((it) => !it.done).length, keywords: keywords.join(' '), target_ages: targetAges, max_age: Math.max(...targetAges), @@ -180,7 +182,6 @@ function convertTransition( return { _id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path. default: false, // We don't work with default transitions but retain this for backwards compatibility. - condition: transition.condition, next: { _id: nextNode._id ? nextNode._id + index : v4(), // Construct a unique ID for the transition for backwards compatibility. hruid: transition.next.learningObjectHruid, @@ -191,6 +192,29 @@ function convertTransition( } } +/** + * Start from the start node and then always take the first transition until there are no transitions anymore. + * Returns the traversed nodes as an array. (This effectively filters outs nodes that cannot be reached.) + */ +function traverseLearningPath(nodes: LearningObjectNode[]): LearningObjectNode[] { + const traversedNodes: LearningObjectNode[] = []; + let currentNode = nodes.find((it) => it.start_node); + + while (currentNode) { + traversedNodes.push(currentNode); + + const next = currentNode.transitions[0]?.next; + + if (next) { + currentNode = nodes.find((it) => it.learningobject_hruid === next.hruid && it.language === next.language && it.version === next.version); + } else { + currentNode = undefined; + } + } + + return traversedNodes; +} + /** * Service providing access to data about learning paths from the database. */ 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 263bffaf..77cae652 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 @@ -62,6 +62,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { data: learningPaths, }; }, + async searchLearningPaths(query: string, language: string, personalizedFor: Group): Promise { const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; const params = { all: query, language }; @@ -75,7 +76,8 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { }, async getLearningPathsAdministratedBy(_adminUsername: string) { - return []; // Learning paths fetched from the Dwengo API cannot be administrated by a user. + // Dwengo API does not have the concept of admins, so we cannot filter by them. + return []; }, }; diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 3ccd2dba..9050e278 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -10,7 +10,7 @@ import { mapToClassDTO } from '../interfaces/class.js'; import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; -import { getAllAssignments } from './assignments.js'; +import { fetchAssignment } from './assignments.js'; import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; import { mapToStudentRequest, mapToStudentRequestDTO } from '../interfaces/student-request.js'; import { Student } from '../entities/users/student.entity.js'; @@ -26,6 +26,7 @@ import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-requ import { ConflictException } from '../exceptions/conflict-exception.js'; import { Submission } from '../entities/assignments/submission.entity.js'; import { mapToUsername } from '../interfaces/user.js'; +import { mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; export async function getAllStudents(full: boolean): Promise { const studentRepository = getStudentRepository(); @@ -50,8 +51,7 @@ export async function fetchStudent(username: string): Promise { } export async function fetchStudents(usernames: string[]): Promise { - const members = await Promise.all(usernames.map(async (username) => await fetchStudent(username))); - return members; + return await Promise.all(usernames.map(async (username) => await fetchStudent(username))); } export async function getStudent(username: string): Promise { @@ -102,10 +102,14 @@ export async function getStudentClasses(username: string, full: boolean): Promis export async function getStudentAssignments(username: string, full: boolean): Promise { const student = await fetchStudent(username); - const classRepository = getClassRepository(); - const classes = await classRepository.findByStudent(student); + const groupRepository = getGroupRepository(); + const groups = await groupRepository.findAllGroupsWithStudent(student); + const assignments = await Promise.all(groups.map(async (group) => await fetchAssignment(group.assignment.within.classId!, group.assignment.id!))); - return (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat(); + if (full) { + return assignments.map(mapToAssignmentDTO); + } + return assignments.map(mapToAssignmentDTOId); } export async function getStudentGroups(username: string, full: boolean): Promise { diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts index 1170bf50..90b245e3 100644 --- a/backend/src/services/submissions.ts +++ b/backend/src/services/submissions.ts @@ -1,12 +1,13 @@ -import { getAssignmentRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; +import { getSubmissionRepository } from '../data/repositories.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; import { fetchStudent } from './students.js'; -import { getExistingGroupFromGroupDTO } from './groups.js'; +import { fetchGroup, getExistingGroupFromGroupDTO } from './groups.js'; import { Submission } from '../entities/assignments/submission.entity.js'; import { Language } from '@dwengo-1/common/util/language'; +import { fetchAssignment } from './assignments.js'; export async function fetchSubmission(loId: LearningObjectIdentifier, submissionNumber: number): Promise { const submissionRepository = getSubmissionRepository(); @@ -64,15 +65,18 @@ export async function getSubmissionsForLearningObjectAndAssignment( groupId?: number ): Promise { const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); - const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, assignmentId); - let submissions: Submission[]; - if (groupId !== undefined) { - const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, groupId); - submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndGroup(loId, group!); - } else { - submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!); + try { + let submissions: Submission[]; + if (groupId !== undefined) { + const group = await fetchGroup(classId, assignmentId, groupId); + submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndGroup(loId, group); + } else { + const assignment = await fetchAssignment(classId, assignmentId); + submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndAssignment(loId, assignment); + } + return submissions.map((s) => mapToSubmissionDTO(s)); + } catch (_) { + return []; } - - return submissions.map((s) => mapToSubmissionDTO(s)); } diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 3d07960f..4d65231d 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -1,4 +1,4 @@ -import { getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js'; +import { getAssignmentRepository, getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js'; import { mapToClassDTO } from '../interfaces/class.js'; import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js'; import { Teacher } from '../entities/users/teacher.entity.js'; @@ -18,6 +18,8 @@ import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; import { ConflictException } from '../exceptions/conflict-exception.js'; +import { AssignmentDTO, AssignmentDTOId } from '@dwengo-1/common/interfaces/assignment'; +import { mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; import { mapToUsername } from '../interfaces/user.js'; export async function getAllTeachers(full: boolean): Promise { @@ -91,6 +93,17 @@ export async function getClassesByTeacher(username: string, full: boolean): Prom return classes.map((cls) => cls.id); } +export async function getTeacherAssignments(username: string, full: boolean): Promise { + const assignmentRepository = getAssignmentRepository(); + const assignments = await assignmentRepository.findAllByResponsibleTeacher(username); + + if (full) { + return assignments.map(mapToAssignmentDTO); + } + + return assignments.map(mapToAssignmentDTOId); +} + export async function getStudentsByTeacher(username: string, full: boolean): Promise { const classes: ClassDTO[] = await fetchClassesByTeacher(username); diff --git a/backend/tests/controllers/students.test.ts b/backend/tests/controllers/students.test.ts index b5ac1e0d..c3d84fd8 100644 --- a/backend/tests/controllers/students.test.ts +++ b/backend/tests/controllers/students.test.ts @@ -14,6 +14,7 @@ import { getStudentRequestsHandler, deleteClassJoinRequestHandler, getStudentRequestHandler, + getStudentAssignmentsHandler, } from '../../src/controllers/students.js'; import { getDireStraits, getNoordkaap, getTheDoors, TEST_STUDENTS } from '../test_assets/users/students.testdata.js'; import { NotFoundException } from '../../src/exceptions/not-found-exception.js'; @@ -150,6 +151,19 @@ describe('Student controllers', () => { expect(result.groups).to.have.length.greaterThan(0); }); + it('Student assignments', async () => { + const group = getTestGroup01(); + const member = group.members[0]; + req = { params: { username: member.username }, query: {} }; + + await getStudentAssignmentsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignments: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.assignments).to.have.length.greaterThan(0); + }); + it('Student submissions', async () => { const submission = getSubmission01(); req = { params: { username: submission.submitter.username }, query: { full: 'true' } }; diff --git a/backend/tests/services/learning-path/database-learning-path-provider.test.ts b/backend/tests/services/learning-path/database-learning-path-provider.test.ts index 2cc594d1..e37b5748 100644 --- a/backend/tests/services/learning-path/database-learning-path-provider.test.ts +++ b/backend/tests/services/learning-path/database-learning-path-provider.test.ts @@ -14,11 +14,14 @@ import { testLearningObjectEssayQuestion, testLearningObjectMultipleChoice, } from '../../test_assets/content/learning-objects.testdata'; -import { testLearningPathWithConditions } from '../../test_assets/content/learning-paths.testdata'; +import { testLearningPath02, testLearningPathWithConditions } from '../../test_assets/content/learning-paths.testdata'; import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service'; import { getTestGroup01, getTestGroup02 } from '../../test_assets/assignments/groups.testdata'; import { Group } from '../../../src/entities/assignments/group.entity.js'; +import { Teacher } from '../../../src/entities/users/teacher.entity.js'; import { RequiredEntityData } from '@mikro-orm/core'; +import { getFooFighters, getLimpBizkit } from '../../test_assets/users/teachers.testdata'; +import { mapToTeacherDTO } from '../../../src/interfaces/teacher'; function expectBranchingObjectNode(result: LearningPathResponse): LearningObjectNode { const branchingObjectMatches = result.data![0].nodes.filter((it) => it.learningobject_hruid === testLearningObjectMultipleChoice.hruid); @@ -33,6 +36,8 @@ describe('DatabaseLearningPathProvider', () => { let finalLearningObject: RequiredEntityData; let groupA: Group; let groupB: Group; + let teacherA: Teacher; + let teacherB: Teacher; beforeAll(async () => { await setupTestApp(); @@ -42,6 +47,8 @@ describe('DatabaseLearningPathProvider', () => { finalLearningObject = testLearningObjectEssayQuestion; groupA = getTestGroup01(); groupB = getTestGroup02(); + teacherA = getFooFighters(); + teacherB = getLimpBizkit(); // Place different submissions for group A and B. const submissionRepo = getSubmissionRepository(); @@ -140,4 +147,18 @@ describe('DatabaseLearningPathProvider', () => { expect(result.length).toBe(0); }); }); + + describe('getLearningPathsAdministratedBy', () => { + it('returns the learning path owned by the admin', async () => { + const expectedLearningPath = mapToLearningPath(testLearningPath02, [mapToTeacherDTO(teacherB)]); + const result = await databaseLearningPathProvider.getLearningPathsAdministratedBy([teacherB], expectedLearningPath.language); + expect(result.length).toBe(1); + expect(result[0].title).toBe(expectedLearningPath.title); + expect(result[0].description).toBe(expectedLearningPath.description); + }); + it('returns an empty result when querying admins that do not have custom learning paths', async () => { + const result = await databaseLearningPathProvider.getLearningPathsAdministratedBy([teacherA], testLearningPath.language); + expect(result.length).toBe(0); + }); + }); }); diff --git a/backend/tests/test_assets/assignments/groups.testdata.ts b/backend/tests/test_assets/assignments/groups.testdata.ts index f2508108..342d8693 100644 --- a/backend/tests/test_assets/assignments/groups.testdata.ts +++ b/backend/tests/test_assets/assignments/groups.testdata.ts @@ -51,7 +51,7 @@ export function makeTestGroups(em: EntityManager): Group[] { */ group05 = em.create(Group, { assignment: getAssignment04(), - groupNumber: 21001, + groupNumber: 21006, members: [getNoordkaap(), getDireStraits()], }); diff --git a/backend/tests/test_assets/content/learning-paths.testdata.ts b/backend/tests/test_assets/content/learning-paths.testdata.ts index 2a0640f8..a89ce60a 100644 --- a/backend/tests/test_assets/content/learning-paths.testdata.ts +++ b/backend/tests/test_assets/content/learning-paths.testdata.ts @@ -14,10 +14,12 @@ import { testLearningObjectMultipleChoice, testLearningObjectPnNotebooks, } from './learning-objects.testdata'; +import { getLimpBizkit } from '../users/teachers.testdata'; export function makeTestLearningPaths(_em: EntityManager): LearningPath[] { const learningPath01 = mapToLearningPath(testLearningPath01, []); const learningPath02 = mapToLearningPath(testLearningPath02, []); + learningPath02.admins = [getLimpBizkit()]; const partiallyDatabasePartiallyDwengoApiLearningPath = mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []); const learningPathWithConditions = mapToLearningPath(testLearningPathWithConditions, []); diff --git a/backend/tool/seedORM.ts b/backend/tool/seedORM.ts index eec73a77..8a3b7a37 100644 --- a/backend/tool/seedORM.ts +++ b/backend/tool/seedORM.ts @@ -5,8 +5,22 @@ import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata'; import { makeTestLearningObjects } from '../tests/test_assets/content/learning-objects.testdata'; import { makeTestLearningPaths } from '../tests/test_assets/content/learning-paths.testdata'; import { makeTestClasses } from '../tests/test_assets/classes/classes.testdata'; -import { makeTestAssignemnts } from '../tests/test_assets/assignments/assignments.testdata'; -import { getTestGroup01, getTestGroup02, getTestGroup03, getTestGroup04, makeTestGroups } from '../tests/test_assets/assignments/groups.testdata'; +import { + getAssignment01, + getAssignment02, + getAssignment04, + getConditionalPathAssignment, + makeTestAssignemnts, +} from '../tests/test_assets/assignments/assignments.testdata'; +import { + getGroup1ConditionalLearningPath, + getTestGroup01, + getTestGroup02, + getTestGroup03, + getTestGroup04, + getTestGroup05, + makeTestGroups, +} from '../tests/test_assets/assignments/groups.testdata'; import { Group } from '../src/entities/assignments/group.entity'; import { makeTestTeacherInvitations } from '../tests/test_assets/classes/teacher-invitations.testdata'; import { makeTestClassJoinRequests } from '../tests/test_assets/classes/class-join-requests.testdata'; @@ -36,8 +50,14 @@ export async function seedORM(orm: MikroORM): Promise { const groups = makeTestGroups(em); - assignments[0].groups = new Collection([getTestGroup01(), getTestGroup02(), getTestGroup03()]); - assignments[1].groups = new Collection([getTestGroup04()]); + let assignment = getAssignment01(); + assignment.groups = new Collection([getTestGroup01(), getTestGroup02(), getTestGroup03()]); + assignment = getAssignment02(); + assignment.groups = new Collection([getTestGroup04()]); + assignment = getAssignment04(); + assignment.groups = new Collection([getTestGroup05()]); + assignment = getConditionalPathAssignment(); + assignment.groups = new Collection([getGroup1ConditionalLearningPath()]); const teacherInvitations = makeTestTeacherInvitations(em); const classJoinRequests = makeTestClassJoinRequests(em); diff --git a/common/package.json b/common/package.json index 90b3dbf4..6c323a6d 100644 --- a/common/package.json +++ b/common/package.json @@ -1,6 +1,6 @@ { "name": "@dwengo-1/common", - "version": "0.2.0", + "version": "1.0.0", "description": "Common types and utilities for Dwengo-1", "private": true, "type": "module", diff --git a/common/src/interfaces/assignment.ts b/common/src/interfaces/assignment.ts index 677221f1..49e2a84b 100644 --- a/common/src/interfaces/assignment.ts +++ b/common/src/interfaces/assignment.ts @@ -7,7 +7,7 @@ export interface AssignmentDTO { description: string; learningPath: string; language: string; - deadline: Date; + deadline: Date | null; groups: GroupDTO[] | string[][]; } diff --git a/common/src/util/match-mode.ts b/common/src/util/match-mode.ts new file mode 100644 index 00000000..5b261f01 --- /dev/null +++ b/common/src/util/match-mode.ts @@ -0,0 +1,10 @@ +export enum MatchMode { + /** + * Match any + */ + ANY = 'ANY', + /** + * Match all + */ + ALL = 'ALL', +} diff --git a/compose.production.yml b/compose.production.yml index 544e527f..be7190a5 100644 --- a/compose.production.yml +++ b/compose.production.yml @@ -60,7 +60,7 @@ services: extends: file: ./compose.yml service: idp - command: ['start', '--http-port', '7080', '--https-port', '7443', '--import-realm'] + command: ['start', '--http-port', '7080', '--https-port', '7443'] networks: - dwengo-1 labels: diff --git a/compose.yml b/compose.yml index 246a35ad..9480f1e4 100644 --- a/compose.yml +++ b/compose.yml @@ -20,7 +20,7 @@ services: image: quay.io/keycloak/keycloak:latest ports: - '7080:7080' - # - '7443:7443' + # - '7443:7443' command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] restart: unless-stopped volumes: diff --git a/docs/api/generate.ts b/docs/api/generate.ts index 07523a32..2805045f 100644 --- a/docs/api/generate.ts +++ b/docs/api/generate.ts @@ -2,7 +2,7 @@ import swaggerAutogen from 'swagger-autogen'; const doc = { info: { - version: '0.1.0', + version: '1.0.0', title: 'Dwengo-1 Backend API', description: 'Dwengo-1 Backend API using Express, based on VZW Dwengo', license: { diff --git a/docs/package.json b/docs/package.json index 62f78270..fd9570be 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "dwengo-1-docs", - "version": "0.2.0", + "version": "1.0.0", "description": "Documentation for Dwengo-1", "private": true, "scripts": { diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 1ddb8dc0..c4de2191 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,3 +1,5 @@ +#syntax=docker/dockerfile:1.7-labs + FROM node:22 AS build-stage # install simple http server for serving static content @@ -26,7 +28,7 @@ RUN npm run build --workspace=common WORKDIR /app/dwengo/frontend -COPY frontend ./ +COPY --exclude=frontend/tests/ frontend ./ RUN npx vite build diff --git a/frontend/package.json b/frontend/package.json index c9bea614..ff6d4c8e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "dwengo-1-frontend", - "version": "0.2.0", + "version": "1.0.0", "description": "Frontend for Dwengo-1", "private": true, "type": "module", @@ -17,7 +17,6 @@ "test:e2e": "playwright test" }, "dependencies": { - "@dwengo-1/common": "^0.2.0", "@tanstack/react-query": "^5.69.0", "@tanstack/vue-query": "^5.69.0", "@vueuse/core": "^13.1.0", diff --git a/frontend/src/assets/assignment.css b/frontend/src/assets/assignment.css index 029dec22..c23e6585 100644 --- a/frontend/src/assets/assignment.css +++ b/frontend/src/assets/assignment.css @@ -18,10 +18,19 @@ font-size: 1.1rem; } -.top-right-btn { - position: absolute; - right: 2%; - color: red; +.top-buttons-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + position: relative; +} + +.right-buttons { + display: flex; + gap: 0.5rem; + align-items: center; + color: #0e6942; } .group-section { diff --git a/frontend/src/components/DiscussionSideBarElement.vue b/frontend/src/components/DiscussionSideBarElement.vue new file mode 100644 index 00000000..2d7eb1df --- /dev/null +++ b/frontend/src/components/DiscussionSideBarElement.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/frontend/src/components/DiscussionsSideBar.vue b/frontend/src/components/DiscussionsSideBar.vue new file mode 100644 index 00000000..d32a2f62 --- /dev/null +++ b/frontend/src/components/DiscussionsSideBar.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/frontend/src/components/DwengoTable.vue b/frontend/src/components/DwengoTable.vue new file mode 100644 index 00000000..b04616e8 --- /dev/null +++ b/frontend/src/components/DwengoTable.vue @@ -0,0 +1,52 @@ + + + diff --git a/frontend/src/components/GroupProgressRow.vue b/frontend/src/components/GroupProgressRow.vue new file mode 100644 index 00000000..2704c494 --- /dev/null +++ b/frontend/src/components/GroupProgressRow.vue @@ -0,0 +1,47 @@ + + + diff --git a/frontend/src/components/GroupSubmissionStatus.vue b/frontend/src/components/GroupSubmissionStatus.vue new file mode 100644 index 00000000..2cb4cb7a --- /dev/null +++ b/frontend/src/components/GroupSubmissionStatus.vue @@ -0,0 +1,58 @@ + + + diff --git a/frontend/src/components/MenuBar.vue b/frontend/src/components/MenuBar.vue index 88b757b7..217555b3 100644 --- a/frontend/src/components/MenuBar.vue +++ b/frontend/src/components/MenuBar.vue @@ -87,14 +87,13 @@ > {{ t("classes") }} - - - - - - - - + + {{ t("discussions") }} + diff --git a/frontend/src/components/assignments/AssignmentCard.vue b/frontend/src/components/assignments/AssignmentCard.vue new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/components/assignments/DeadlineSelector.vue b/frontend/src/components/assignments/DeadlineSelector.vue index 304c544c..fb6e3cbd 100644 --- a/frontend/src/components/assignments/DeadlineSelector.vue +++ b/frontend/src/components/assignments/DeadlineSelector.vue @@ -1,18 +1,42 @@ diff --git a/frontend/src/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue b/frontend/src/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue index 5a7fa9d2..7b220b04 100644 --- a/frontend/src/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue +++ b/frontend/src/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue @@ -18,6 +18,15 @@ version: number; group: { forGroup: number; assignmentNo: number; classId: string }; }>(); + + function parseContent(content: string): SubmissionData { + if (content === "") { + return []; + } + + return JSON.parse(content); + } + const emit = defineEmits<(e: "update:submissionData", value: SubmissionData) => void>(); const submissionQuery = useSubmissionsQuery( @@ -35,7 +44,7 @@ } function emitSubmission(submission: SubmissionDTO): void { - emitSubmissionData(JSON.parse(submission.content)); + emitSubmissionData(parseContent(submission.content)); } watch(submissionQuery.data, () => { @@ -47,12 +56,13 @@ } }); - const lastSubmission = computed(() => { + const lastSubmission = computed(() => { const submissions = submissionQuery.data.value; if (!submissions || submissions.length === 0) { return undefined; } - return JSON.parse(submissions[submissions.length - 1].content); + + return parseContent(submissions[submissions.length - 1].content); }); const showSubmissionTable = computed(() => props.submissionData !== undefined && props.submissionData.length > 0); diff --git a/frontend/src/views/own-learning-content/learning-paths/LearningPathPreviewCard.vue b/frontend/src/views/own-learning-content/learning-paths/LearningPathPreviewCard.vue index b4328d56..01812afe 100644 --- a/frontend/src/views/own-learning-content/learning-paths/LearningPathPreviewCard.vue +++ b/frontend/src/views/own-learning-content/learning-paths/LearningPathPreviewCard.vue @@ -89,6 +89,15 @@ props.selectedLearningPath.language !== parsedLearningPath.value.language), ); + const selectedLearningPathLink = computed(() => { + if (!props.selectedLearningPath) { + return undefined; + } + const { hruid, language } = props.selectedLearningPath; + const startNode = props.selectedLearningPath.nodes.find((it) => it.start_node); + return `/learningPath/${hruid}/${language}/${startNode.learningobject_hruid}`; + }); + function getErrorMessage(): string | null { if (postError.value) { return t(extractErrorMessage(postError.value)); @@ -104,7 +113,43 @@ - + diff --git a/frontend/tests/controllers/classes-controller.test.ts b/frontend/tests/controllers/classes-controller.test.ts index 8768aee8..82c95e17 100644 --- a/frontend/tests/controllers/classes-controller.test.ts +++ b/frontend/tests/controllers/classes-controller.test.ts @@ -1,10 +1,58 @@ -import { describe, expect, it } from "vitest"; +import { describe, it, expect, beforeEach } from "vitest"; import { ClassController } from "../../src/controllers/classes"; -describe("Test controller classes", () => { - it("Get classes", async () => { - const controller = new ClassController(); - const data = await controller.getAll(true); - expect(data.classes).to.have.length.greaterThan(0); +describe("ClassController Tests", () => { + let controller: ClassController; + const testClassId = "X2J9QT"; + + beforeEach(() => { + controller = new ClassController(); + }); + + it("should fetch all classes", async () => { + const result = await controller.getAll(true); + expect(result).toHaveProperty("classes"); + expect(Array.isArray(result.classes)).toBe(true); + expect(result.classes.length).toBeGreaterThan(0); + }); + + it("should fetch a class by ID", async () => { + const result = await controller.getById(testClassId); + expect(result).toHaveProperty("class"); + expect(result.class).toHaveProperty("id", testClassId); + }); + + it("should fetch students for a class", async () => { + const result = await controller.getStudents(testClassId, true); + expect(result).toHaveProperty("students"); + expect(Array.isArray(result.students)).toBe(true); + }); + + it("should fetch teachers for a class", async () => { + const result = await controller.getTeachers(testClassId, true); + expect(result).toHaveProperty("teachers"); + expect(Array.isArray(result.teachers)).toBe(true); + }); + + it("should fetch teacher invitations for a class", async () => { + const result = await controller.getTeacherInvitations(testClassId, true); + expect(result).toHaveProperty("invitations"); + expect(Array.isArray(result.invitations)).toBe(true); + }); + + it("should fetch assignments for a class", async () => { + const result = await controller.getAssignments(testClassId, true); + expect(result).toHaveProperty("assignments"); + expect(Array.isArray(result.assignments)).toBe(true); + }); + + it("should handle fetching a non-existent class", async () => { + const nonExistentId = "NON_EXISTENT_ID"; + await expect(controller.getById(nonExistentId)).rejects.toThrow(); + }); + + it("should handle deleting a non-existent class", async () => { + const nonExistentId = "NON_EXISTENT_ID"; + await expect(controller.deleteClass(nonExistentId)).rejects.toThrow(); }); }); diff --git a/frontend/tests/utils/array-utils.test.ts b/frontend/tests/utils/array-utils.test.ts new file mode 100644 index 00000000..ee810d3a --- /dev/null +++ b/frontend/tests/utils/array-utils.test.ts @@ -0,0 +1,49 @@ +import { copyArrayWith } from "../../src/utils/array-utils"; +import { describe, it, expect } from "vitest"; + +describe("copyArrayWith", () => { + it("should replace the element at the specified index", () => { + const original = [1, 2, 3, 4]; + const result = copyArrayWith(2, 99, original); + expect(result).toEqual([1, 2, 99, 4]); + }); + + it("should not modify the original array", () => { + const original = ["a", "b", "c"]; + const result = copyArrayWith(1, "x", original); + expect(original).toEqual(["a", "b", "c"]); // Original remains unchanged + expect(result).toEqual(["a", "x", "c"]); + }); + + it("should handle replacing the first element", () => { + const original = [true, false, true]; + const result = copyArrayWith(0, false, original); + expect(result).toEqual([false, false, true]); + }); + + it("should handle replacing the last element", () => { + const original = ["apple", "banana", "cherry"]; + const result = copyArrayWith(2, "date", original); + expect(result).toEqual(["apple", "banana", "date"]); + }); + + it("should work with complex objects", () => { + const original = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const newValue = { id: 99 }; + const result = copyArrayWith(1, newValue, original); + expect(result).toEqual([{ id: 1 }, { id: 99 }, { id: 3 }]); + expect(original[1].id).toBe(2); // Original remains unchanged + }); + + it("should allow setting to undefined", () => { + const original = [1, 2, 3]; + const result = copyArrayWith(1, undefined, original); + expect(result).toEqual([1, undefined, 3]); + }); + + it("should allow setting to null", () => { + const original = [1, 2, 3]; + const result = copyArrayWith(1, null, original); + expect(result).toEqual([1, null, 3]); + }); +}); diff --git a/frontend/tests/utils/assignment-utils.test.ts b/frontend/tests/utils/assignment-utils.test.ts new file mode 100644 index 00000000..42140638 --- /dev/null +++ b/frontend/tests/utils/assignment-utils.test.ts @@ -0,0 +1,86 @@ +import { LearningPathNode } from "@dwengo-1/backend/dist/entities/content/learning-path-node.entity"; +import { calculateProgress } from "../../src/utils/assignment-utils"; +import { LearningPath } from "../../src/data-objects/learning-paths/learning-path"; +import { describe, it, expect } from "vitest"; + +describe("calculateProgress", () => { + it("should return 0 when no nodes are completed", () => { + const lp = new LearningPath({ + language: "en", + hruid: "test-path", + title: "Test Path", + description: "Test Description", + amountOfNodes: 10, + amountOfNodesLeft: 10, + keywords: ["test"], + targetAges: { min: 10, max: 15 }, + startNode: {} as LearningPathNode, + }); + + expect(calculateProgress(lp)).toBe(0); + }); + + it("should return 100 when all nodes are completed", () => { + const lp = new LearningPath({ + language: "en", + hruid: "test-path", + title: "Test Path", + description: "Test Description", + amountOfNodes: 10, + amountOfNodesLeft: 0, + keywords: ["test"], + targetAges: { min: 10, max: 15 }, + startNode: {} as LearningPathNode, + }); + + expect(calculateProgress(lp)).toBe(100); + }); + + it("should return 50 when half of the nodes are completed", () => { + const lp = new LearningPath({ + language: "en", + hruid: "test-path", + title: "Test Path", + description: "Test Description", + amountOfNodes: 10, + amountOfNodesLeft: 5, + keywords: ["test"], + targetAges: { min: 10, max: 15 }, + startNode: {} as LearningPathNode, + }); + + expect(calculateProgress(lp)).toBe(50); + }); + + it("should handle floating point progress correctly", () => { + const lp = new LearningPath({ + language: "en", + hruid: "test-path", + title: "Test Path", + description: "Test Description", + amountOfNodes: 3, + amountOfNodesLeft: 1, + keywords: ["test"], + targetAges: { min: 10, max: 15 }, + startNode: {} as LearningPathNode, + }); + + expect(calculateProgress(lp)).toBeCloseTo(66.666, 2); + }); + + it("should handle edge case where amountOfNodesLeft is negative", () => { + const lp = new LearningPath({ + language: "en", + hruid: "test-path", + title: "Test Path", + description: "Test Description", + amountOfNodes: 10, + amountOfNodesLeft: -5, + keywords: ["test"], + targetAges: { min: 10, max: 15 }, + startNode: {} as LearningPathNode, + }); + + expect(calculateProgress(lp)).toBe(150); + }); +}); diff --git a/frontend/tests/utils/assingment-rules.test.ts b/frontend/tests/utils/assingment-rules.test.ts deleted file mode 100644 index 3b6e5bfd..00000000 --- a/frontend/tests/utils/assingment-rules.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - assignmentTitleRules, - classRules, - deadlineRules, - descriptionRules, - learningPathRules, -} from "../../src/utils/assignment-rules"; - -describe("Validation Rules", () => { - describe("assignmentTitleRules", () => { - it("should return true for a valid title", () => { - const result = assignmentTitleRules[0]("Valid Title"); - expect(result).toBe(true); - }); - - it("should return an error message for an empty title", () => { - const result = assignmentTitleRules[0](""); - expect(result).toBe("Title cannot be empty."); - }); - }); - - describe("learningPathRules", () => { - it("should return true for a valid learning path", () => { - const result = learningPathRules[0]({ hruid: "123", title: "Path Title" }); - expect(result).toBe(true); - }); - - it("should return an error message for an invalid learning path", () => { - const result = learningPathRules[0]({ hruid: "", title: "" }); - expect(result).toBe("You must select a learning path."); - }); - }); - - describe("classRules", () => { - it("should return true for a valid class", () => { - const result = classRules[0]("Class 1"); - expect(result).toBe(true); - }); - - it("should return an error message for an empty class", () => { - const result = classRules[0](""); - expect(result).toBe("You must select at least one class."); - }); - }); - - describe("deadlineRules", () => { - it("should return true for a valid future deadline", () => { - const futureDate = new Date(Date.now() + 1000 * 60 * 60).toISOString(); - const result = deadlineRules[0](futureDate); - expect(result).toBe(true); - }); - - it("should return an error message for a past deadline", () => { - const pastDate = new Date(Date.now() - 1000 * 60 * 60).toISOString(); - const result = deadlineRules[0](pastDate); - expect(result).toBe("The deadline must be in the future."); - }); - - it("should return an error message for an invalid date", () => { - const result = deadlineRules[0]("invalid-date"); - expect(result).toBe("Invalid date or time."); - }); - - it("should return an error message for an empty deadline", () => { - const result = deadlineRules[0](""); - expect(result).toBe("You must set a deadline."); - }); - }); - - describe("descriptionRules", () => { - it("should return true for a valid description", () => { - const result = descriptionRules[0]("This is a valid description."); - expect(result).toBe(true); - }); - - it("should return an error message for an empty description", () => { - const result = descriptionRules[0](""); - expect(result).toBe("Description cannot be empty."); - }); - }); -}); diff --git a/package-lock.json b/package-lock.json index 898f596d..df6b6839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dwengo-1", - "version": "0.2.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dwengo-1", - "version": "0.2.0", + "version": "1.0.0", "license": "MIT", "workspaces": [ "backend", @@ -29,7 +29,7 @@ }, "backend": { "name": "@dwengo-1/backend", - "version": "0.2.0", + "version": "1.0.0", "dependencies": { "@mikro-orm/core": "6.4.12", "@mikro-orm/knex": "6.4.12", @@ -95,20 +95,19 @@ }, "common": { "name": "@dwengo-1/common", - "version": "0.2.0" + "version": "1.0.0" }, "docs": { "name": "dwengo-1-docs", - "version": "0.2.0", + "version": "1.0.0", "devDependencies": { "swagger-autogen": "^2.23.7" } }, "frontend": { "name": "dwengo-1-frontend", - "version": "0.2.0", + "version": "1.0.0", "dependencies": { - "@dwengo-1/common": "^0.2.0", "@tanstack/react-query": "^5.69.0", "@tanstack/vue-query": "^5.69.0", "@vueuse/core": "^13.1.0", @@ -184,9 +183,9 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.7.tgz", - "integrity": "sha512-Ok5fYhtwdyJQmU1PpEv6Si7Y+A4cYb8yNM9oiIJC9TzXPMuN9fvdonKJqcnz9TbFqV6bQ8z0giRq0iaOpGZV2g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "license": "MIT", "dependencies": { "@csstools/css-calc": "^2.1.3", @@ -2515,9 +2514,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", "cpu": [ "arm" ], @@ -2528,9 +2527,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", "cpu": [ "arm64" ], @@ -2541,9 +2540,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", "cpu": [ "arm64" ], @@ -2554,9 +2553,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", "cpu": [ "x64" ], @@ -2567,9 +2566,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", "cpu": [ "arm64" ], @@ -2580,9 +2579,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", "cpu": [ "x64" ], @@ -2593,9 +2592,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", "cpu": [ "arm" ], @@ -2606,9 +2605,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", "cpu": [ "arm" ], @@ -2619,9 +2618,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", "cpu": [ "arm64" ], @@ -2632,9 +2631,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", + "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", "cpu": [ "arm64" ], @@ -2645,9 +2644,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", "cpu": [ "loong64" ], @@ -2658,9 +2657,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", "cpu": [ "ppc64" ], @@ -2671,9 +2670,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", "cpu": [ "riscv64" ], @@ -2684,9 +2683,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", "cpu": [ "riscv64" ], @@ -2697,9 +2696,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", "cpu": [ "s390x" ], @@ -2710,9 +2709,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", + "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", "cpu": [ "x64" ], @@ -2723,9 +2722,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", + "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", "cpu": [ "x64" ], @@ -2736,9 +2735,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", "cpu": [ "arm64" ], @@ -2749,9 +2748,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", "cpu": [ "ia32" ], @@ -2762,9 +2761,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", "cpu": [ "x64" ], @@ -2954,9 +2953,9 @@ "license": "MIT" }, "node_modules/@tsconfig/node22": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz", - "integrity": "sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg==", + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.2.tgz", + "integrity": "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA==", "dev": true, "license": "MIT" }, @@ -3013,9 +3012,9 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", - "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", + "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", "dev": true, "license": "MIT", "dependencies": { @@ -3120,18 +3119,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", - "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "version": "22.15.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.20.tgz", + "integrity": "sha512-A6BohGFRGHAscJsTslDCA9JG7qSJr/DWUvrvY8yi9IgnGtMxCyat7vvQ//MFa0DnLsyuS3wYTpLdw4Hf+Q5JXw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "license": "MIT" }, "node_modules/@types/range-parser": { @@ -3427,9 +3426,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz", - "integrity": "sha512-cj76U5gXCl3g88KSnf80kof6+6w+K4BjOflCl7t6yRJPDuCrHtVu0SgNYOUARJOL5TI8RScDbm5x4s1/P9bvpw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", + "integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", "dev": true, "license": "MIT", "dependencies": { @@ -3450,8 +3449,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.1.3", - "vitest": "3.1.3" + "@vitest/browser": "3.1.4", + "vitest": "3.1.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3481,14 +3480,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -3497,13 +3496,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -3534,9 +3533,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3547,13 +3546,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", + "@vitest/utils": "3.1.4", "pathe": "^2.0.3" }, "funding": { @@ -3561,13 +3560,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -3576,9 +3575,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -3589,13 +3588,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -5302,9 +5301,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", - "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -6745,9 +6744,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7393,12 +7392,12 @@ "license": "ISC" }, "node_modules/isomorphic-dompurify": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.24.0.tgz", - "integrity": "sha512-SgKoDBCQveodymGMBPpzs9MOTCk4Luq0bTfwoPrUKa7q0FnCLZMtqR25Rnq228zJfMTsX1ZItiJbDtjb2lyv4A==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.25.0.tgz", + "integrity": "sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==", "license": "MIT", "dependencies": { - "dompurify": "^3.2.5", + "dompurify": "^3.2.6", "jsdom": "^26.1.0" }, "engines": { @@ -7804,9 +7803,9 @@ } }, "node_modules/jwks-rsa/node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", + "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -8277,9 +8276,9 @@ } }, "node_modules/marked": { - "version": "15.0.11", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.11.tgz", - "integrity": "sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==", + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -9710,9 +9709,9 @@ "license": "ISC" }, "node_modules/protobufjs": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz", - "integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==", + "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": { @@ -10079,9 +10078,9 @@ } }, "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", + "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.7" @@ -10094,26 +10093,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.41.0", + "@rollup/rollup-android-arm64": "4.41.0", + "@rollup/rollup-darwin-arm64": "4.41.0", + "@rollup/rollup-darwin-x64": "4.41.0", + "@rollup/rollup-freebsd-arm64": "4.41.0", + "@rollup/rollup-freebsd-x64": "4.41.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", + "@rollup/rollup-linux-arm-musleabihf": "4.41.0", + "@rollup/rollup-linux-arm64-gnu": "4.41.0", + "@rollup/rollup-linux-arm64-musl": "4.41.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-musl": "4.41.0", + "@rollup/rollup-linux-s390x-gnu": "4.41.0", + "@rollup/rollup-linux-x64-gnu": "4.41.0", + "@rollup/rollup-linux-x64-musl": "4.41.0", + "@rollup/rollup-win32-arm64-msvc": "4.41.0", + "@rollup/rollup-win32-ia32-msvc": "4.41.0", + "@rollup/rollup-win32-x64-msvc": "4.41.0", "fsevents": "~2.3.2" } }, @@ -10917,9 +10916,9 @@ } }, "node_modules/svelte": { - "version": "5.30.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz", - "integrity": "sha512-QIYtKnJGkubWXtNkrUBKVCvyo9gjcccdbnvXfwsGNhvbeNNdQjRDTa/BiQcJ2kWXbXPQbWKyT7CUu53KIj1rfw==", + "version": "5.32.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.32.1.tgz", + "integrity": "sha512-tT02QOeF0dbSIQ+/rUZw+76DyO6ATHvZJGOM2A/Ed6fBwZwUxqIun3beErpePAtwFIK3Mi9k2QAnhFVvUBun8g==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -11044,14 +11043,13 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.11.5", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.5.tgz", - "integrity": "sha512-frqvfWyDA5VPVdrWfH24uM6SI/O8NLpVbIIJxb8t/a3YGsp4AW9CYgSKC0OaSEfexnp7Y1pVh2Y6IHO8ggGDmA==", + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.6.tgz", + "integrity": "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.4", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -11849,9 +11847,9 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "dev": true, "license": "MIT", "dependencies": { @@ -11991,19 +11989,19 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -12016,7 +12014,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -12032,8 +12030,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index 3bf3a17c..260e1411 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dwengo-1", - "version": "0.2.0", + "version": "1.0.0", "description": "Monorepo for Dwengo-1", "private": true, "type": "module",