From 579d657052ef12f5516587d7b4d2a3b1e94fde34 Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Wed, 26 Mar 2025 21:24:05 -0400 Subject: [PATCH 01/12] User Schema, routes for marking and fetching num completed --- .../middlewares/validators/userValidators.ts | 20 +++++ backend/models/user.mgmodel.ts | 11 +++ backend/rest/userRoutes.ts | 77 +++++++++++++++++++ .../implementations/courseModuleService.ts | 3 +- .../services/implementations/userService.ts | 24 +++++- backend/services/interfaces/userService.ts | 9 ++- backend/types/userTypes.ts | 1 + 7 files changed, 140 insertions(+), 5 deletions(-) diff --git a/backend/middlewares/validators/userValidators.ts b/backend/middlewares/validators/userValidators.ts index c2a16515..42ff2481 100644 --- a/backend/middlewares/validators/userValidators.ts +++ b/backend/middlewares/validators/userValidators.ts @@ -119,6 +119,23 @@ export const addBookmarkValidator = async ( return next(); }; +export const completeActivityValidator = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + if (!validatePrimitive(req.body.unitId, "string")) { + return res.status(400).send(getApiValidationError("unitId", "string")); + } + if (!validatePrimitive(req.body.moduleId, "string")) { + return res.status(400).send(getApiValidationError("moduleId", "string")); + } + if (!validatePrimitive(req.body.pageId, "string")) { + return res.status(400).send(getApiValidationError("pageId", "string")); + } + return next(); +}; + export const deleteBookmarkValidator = async ( req: Request, res: Response, @@ -127,5 +144,8 @@ export const deleteBookmarkValidator = async ( if (!validatePrimitive(req.body.pageId, "string")) { return res.status(400).send(getApiValidationError("pageId", "string")); } + if (!validatePrimitive(req.body.activityId, "string")) { + return res.status(400).send(getApiValidationError("activityId", "string")); + } return next(); }; diff --git a/backend/models/user.mgmodel.ts b/backend/models/user.mgmodel.ts index 4f1d16c7..d7eb60f0 100644 --- a/backend/models/user.mgmodel.ts +++ b/backend/models/user.mgmodel.ts @@ -25,6 +25,7 @@ export interface Bookmark { export interface Learner extends User { facilitator: ObjectId; + activitiesCompleted: Map>>; } export interface Facilitator extends User { @@ -123,6 +124,16 @@ const LearnerSchema = new Schema( ref: "User", required: true, }, + activitiesCompleted: { + type: Map, + of: [ + { + type: Map, + of: [{ type: mongoose.Schema.Types.ObjectId, ref: "Activity" }], + }, + ], + default: {}, + }, }, options, ); diff --git a/backend/rest/userRoutes.ts b/backend/rest/userRoutes.ts index 5f262ddf..dda292b9 100644 --- a/backend/rest/userRoutes.ts +++ b/backend/rest/userRoutes.ts @@ -4,6 +4,7 @@ import multer from "multer"; import { getAccessToken, isAuthorizedByRole } from "../middlewares/auth"; import { addBookmarkValidator, + completeActivityValidator, createUserDtoValidator, deleteBookmarkValidator, updateUserAccountValidator, @@ -14,17 +15,21 @@ import UserModel, { Bookmark } from "../models/user.mgmodel"; import nodemailerConfig from "../nodemailer.config"; import AuthService from "../services/implementations/authService"; import CoursePageService from "../services/implementations/coursePageService"; +import CourseModuleService from "../services/implementations/courseModuleService"; +import CourseUnitService from "../services/implementations/courseUnitService"; import EmailService from "../services/implementations/emailService"; import FileStorageService from "../services/implementations/fileStorageService"; import UserService from "../services/implementations/userService"; import IAuthService from "../services/interfaces/authService"; import ICoursePageService from "../services/interfaces/coursePageService"; +import ICourseModuleService from "../services/interfaces/courseModuleService"; import IEmailService from "../services/interfaces/emailService"; import IUserService from "../services/interfaces/userService"; import { UpdateUserDTO, UserDTO, isFacilitator, + isLearner, isRole, } from "../types/userTypes"; import { getErrorMessage } from "../utilities/errorUtils"; @@ -40,6 +45,7 @@ const userService: IUserService = new UserService(); const emailService: IEmailService = new EmailService(nodemailerConfig); const authService: IAuthService = new AuthService(userService, emailService); const coursePageService: ICoursePageService = new CoursePageService(); +const courseModuleService: ICourseModuleService = new CourseModuleService(); const firebaseStorageService: FileStorageService = new FileStorageService( process.env.FIREBASE_STORAGE_DEFAULT_BUCKET || "", ); @@ -476,4 +482,75 @@ userRouter.put( }, ); +userRouter.put( + "/learner/markActivityAsCompleted", + isAuthorizedByRole(new Set(["Learner"])), + completeActivityValidator, + async (req, res) => { + const accessToken = getAccessToken(req)!; + try { + const learnerId = await authService.getUserIdFromAccessToken(accessToken); + const learner = await userService.getUserById(learnerId.toString()); + + if (!isLearner(learner)) { + return res.status(403).send("Forbidden: User is not a learner."); + } + + const unitId = req.body.unitId; + const moduleId = req.body.moduleId; + const activityId = req.body.activityId; + + // Check that everything exists + const courseUnitService = new CourseUnitService(); + const courseUnit = await courseUnitService.getCourseUnit(unitId); + + if (!courseUnit) { + return res.status(400).send("The unit provided does not exist."); + } + + if (!courseUnit.modules.includes(moduleId)) { + return res.status(400).send("The module provided does not exist."); + } + + const courseModule = (await courseModuleService.getCourseModule(moduleId))!; + + if (!courseModule.pages.includes(activityId)) { + return res.status(400).send("The activity provided does not exist."); + } + + const updatedUser = await UserModel.findByIdAndUpdate(learnerId, { + $addToSet: { + [`activitiesCompleted.${unitId}.${moduleId}`]: activityId, + }, + }, { new: true }); + + res.status(200).send(updatedUser); + } catch (error) { + res.status(500).send(getErrorMessage(error)); + } + } +); + +userRouter.get( + "/learner/getCompletedActivities", + isAuthorizedByRole(new Set(["Learner"])), + async (req, res) => { + const accessToken = getAccessToken(req)!; + try { + const learnerId = await authService.getUserIdFromAccessToken(accessToken); + const learner = await userService.getUserById(learnerId.toString()); + + if (!isLearner(learner)) { + return res.status(403).send("Forbidden: User is not a learner."); + } + + const numCompletedModules = await userService.getNumCompletedModules(learner); + + res.status(200).send(numCompletedModules); + } catch (error) { + res.status(500).send(getErrorMessage(error)); + } + } +); + export default userRouter; diff --git a/backend/services/implementations/courseModuleService.ts b/backend/services/implementations/courseModuleService.ts index 8305ac6b..39861083 100644 --- a/backend/services/implementations/courseModuleService.ts +++ b/backend/services/implementations/courseModuleService.ts @@ -6,6 +6,7 @@ import MgCourseModule, { CourseModule, } from "../../models/coursemodule.mgmodel"; import CoursePageModel, { + CoursePage, LessonPageModel, } from "../../models/coursepage.mgmodel"; import MgCourseUnit, { CourseUnit } from "../../models/courseunit.mgmodel"; @@ -101,7 +102,7 @@ class CourseModuleService implements ICourseModuleService { const lessonPdfUrl: string | undefined = await fileStorageService.getFile( `course/pdfs/module-${courseModuleId}.pdf`, ); - const fetchPage = async (page: Schema.Types.ObjectId) => { + const fetchPage = async (page: Schema.Types.ObjectId): Promise => { const pageObject = await CoursePageModel.findById(page).lean().exec(); if (!pageObject) { throw new Error(`Page with id ${page} not found.`); diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index ae215eee..39f76926 100644 --- a/backend/services/implementations/userService.ts +++ b/backend/services/implementations/userService.ts @@ -1,11 +1,10 @@ import * as firebaseAdmin from "firebase-admin"; import { Model, ObjectId } from "mongoose"; -import IUserService from "../interfaces/userService"; import MgUser, { + FacilitatorModel, Learner, - User, LearnerModel, - FacilitatorModel, + User, } from "../../models/user.mgmodel"; import { CreateUserDTO, @@ -18,8 +17,11 @@ import { import { AuthErrorCodes } from "../../types/authTypes"; import { getErrorCode, getErrorMessage } from "../../utilities/errorUtils"; import logger from "../../utilities/logger"; +import IUserService from "../interfaces/userService"; +import CourseModuleService from "./courseModuleService"; const Logger = logger(__filename); +const courseModuleService = new CourseModuleService(); const getMongoUserByAuthId = async (authId: string): Promise => { const user: User | null = await MgUser.findOne({ authId }); @@ -360,6 +362,22 @@ class UserService implements IUserService { throw error; } } + + async getNumCompletedModules(learner: LearnerDTO): Promise { + const numCompletedModules = Array.from( + learner.activitiesCompleted.values(), + ).reduce((acc, curr) => { + const completedModules = Array.from(curr.entries()).filter( + async ([moduleId, activitiesCompletedInModule]) => { + const module = await courseModuleService.getCourseModule(moduleId); + return activitiesCompletedInModule.length === module?.pages.length; + }, + ); + return acc + completedModules.length; + }, 0); + + return numCompletedModules; + } } export default UserService; diff --git a/backend/services/interfaces/userService.ts b/backend/services/interfaces/userService.ts index 32889085..3833040b 100644 --- a/backend/services/interfaces/userService.ts +++ b/backend/services/interfaces/userService.ts @@ -1,6 +1,7 @@ import { ObjectId } from "mongoose"; import { CreateUserDTO, + LearnerDTO, Role, Status, UpdateUserDTO, @@ -118,6 +119,12 @@ interface IUserService { * @param newStatus status to update to */ changeUserStatus(accessToken: string, newStatus: Status): Promise; -} + /** + * Get the number of completed modules for a learner + * @param learner the learner to get the number of completed modules for + * @returns the number of completed modules + */ + getNumCompletedModules(learner: LearnerDTO): Promise; +} export default IUserService; diff --git a/backend/types/userTypes.ts b/backend/types/userTypes.ts index 60ce1268..cf9e8585 100644 --- a/backend/types/userTypes.ts +++ b/backend/types/userTypes.ts @@ -56,6 +56,7 @@ export function isFacilitator(user: UserDTO): user is FacilitatorDTO { export type LearnerDTO = UserDTO & { facilitator: string; + activitiesCompleted: Map>; }; export function isLearner(user: UserDTO): user is LearnerDTO { From e47f08771d06ab3fb52d30a471cc0f12073dfe91 Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Thu, 27 Mar 2025 17:39:35 -0400 Subject: [PATCH 02/12] Add service method to delete an activity from all learners' progress --- backend/rest/userRoutes.ts | 22 ++++++++++++------- .../services/implementations/userService.ts | 10 +++++++++ backend/services/interfaces/userService.ts | 9 ++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/backend/rest/userRoutes.ts b/backend/rest/userRoutes.ts index dda292b9..b7bd9948 100644 --- a/backend/rest/userRoutes.ts +++ b/backend/rest/userRoutes.ts @@ -23,6 +23,7 @@ import UserService from "../services/implementations/userService"; import IAuthService from "../services/interfaces/authService"; import ICoursePageService from "../services/interfaces/coursePageService"; import ICourseModuleService from "../services/interfaces/courseModuleService"; +import ICourseUnitService from "../services/interfaces/courseUnitService"; import IEmailService from "../services/interfaces/emailService"; import IUserService from "../services/interfaces/userService"; import { @@ -45,6 +46,7 @@ const userService: IUserService = new UserService(); const emailService: IEmailService = new EmailService(nodemailerConfig); const authService: IAuthService = new AuthService(userService, emailService); const coursePageService: ICoursePageService = new CoursePageService(); +const courseUnitService: ICourseUnitService = new CourseUnitService(); const courseModuleService: ICourseModuleService = new CourseModuleService(); const firebaseStorageService: FileStorageService = new FileStorageService( process.env.FIREBASE_STORAGE_DEFAULT_BUCKET || "", @@ -496,9 +498,7 @@ userRouter.put( return res.status(403).send("Forbidden: User is not a learner."); } - const unitId = req.body.unitId; - const moduleId = req.body.moduleId; - const activityId = req.body.activityId; + const { unitId, moduleId, activityId } = req.body; // Check that everything exists const courseUnitService = new CourseUnitService(); @@ -509,13 +509,13 @@ userRouter.put( } if (!courseUnit.modules.includes(moduleId)) { - return res.status(400).send("The module provided does not exist."); + return res.status(400).send(`The module provided does not exist on unit ${unitId}.`); } const courseModule = (await courseModuleService.getCourseModule(moduleId))!; if (!courseModule.pages.includes(activityId)) { - return res.status(400).send("The activity provided does not exist."); + return res.status(400).send(`The activity provided does not exist on module ${moduleId}.`); } const updatedUser = await UserModel.findByIdAndUpdate(learnerId, { @@ -524,7 +524,7 @@ userRouter.put( }, }, { new: true }); - res.status(200).send(updatedUser); + res.status(200).json(updatedUser); } catch (error) { res.status(500).send(getErrorMessage(error)); } @@ -532,7 +532,7 @@ userRouter.put( ); userRouter.get( - "/learner/getCompletedActivities", + "/learner/progress", isAuthorizedByRole(new Set(["Learner"])), async (req, res) => { const accessToken = getAccessToken(req)!; @@ -545,8 +545,14 @@ userRouter.get( } const numCompletedModules = await userService.getNumCompletedModules(learner); + const allUnits = await courseUnitService.getCourseUnits(); + const numModules = allUnits.reduce((acc, unit) => acc + unit.modules.length, 0); - res.status(200).send(numCompletedModules); + res.status(200).send({ + numCompletedModules, + numModules, + progressPercentage: (numCompletedModules / numModules) * 100, + }); } catch (error) { res.status(500).send(getErrorMessage(error)); } diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index 39f76926..d06fd5bb 100644 --- a/backend/services/implementations/userService.ts +++ b/backend/services/implementations/userService.ts @@ -378,6 +378,16 @@ class UserService implements IUserService { return numCompletedModules; } + + async deleteActivityFromProgress(unitId: string, moduleId: string, activityId: string): Promise { + const { modifiedCount } = await LearnerModel.updateMany( + {}, + { + $pull: { [`activitiesCompleted.${unitId}.${moduleId}`]: activityId } + } + ) + return modifiedCount; + } } export default UserService; diff --git a/backend/services/interfaces/userService.ts b/backend/services/interfaces/userService.ts index 3833040b..52fe5887 100644 --- a/backend/services/interfaces/userService.ts +++ b/backend/services/interfaces/userService.ts @@ -126,5 +126,14 @@ interface IUserService { * @returns the number of completed modules */ getNumCompletedModules(learner: LearnerDTO): Promise; + + /** + * Delete an activity from every learner's progress + * @param unitId the unit id of the activity to delete + * @param moduleId the module id of the activity to delete + * @param activityId the activity id of the activity to delete + * @returns the number of learners that were updated + */ + deleteActivityFromProgress(unitId: string, moduleId: string, activityId: string): Promise; } export default IUserService; From 8f871247b555dcfad95b73c4b397581352bebec5 Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Thu, 27 Mar 2025 23:20:35 -0400 Subject: [PATCH 03/12] Add unit tests for getting user progress service method --- backend/__mocks__/firebase-admin/storage.ts | 5 + backend/__mocks__/mockData.ts | 108 +++ backend/package.json | 2 +- .../__tests__/courseModuleService.test.ts | 14 +- .../__tests__/userService.test.ts | 52 +- .../implementations/fileStorageService.ts | 14 +- .../services/implementations/userService.ts | 23 +- backend/yarn.lock | 887 +++++++----------- 8 files changed, 532 insertions(+), 573 deletions(-) create mode 100644 backend/__mocks__/firebase-admin/storage.ts create mode 100644 backend/__mocks__/mockData.ts diff --git a/backend/__mocks__/firebase-admin/storage.ts b/backend/__mocks__/firebase-admin/storage.ts new file mode 100644 index 00000000..790b5130 --- /dev/null +++ b/backend/__mocks__/firebase-admin/storage.ts @@ -0,0 +1,5 @@ +const getDownloadURL = jest.fn().mockReturnValue("https://test.com/image.jpg"); + +module.exports = { + getDownloadURL +}; diff --git a/backend/__mocks__/mockData.ts b/backend/__mocks__/mockData.ts new file mode 100644 index 00000000..0cc783f0 --- /dev/null +++ b/backend/__mocks__/mockData.ts @@ -0,0 +1,108 @@ +import { LearnerDTO, Role, Status } from "../types/userTypes"; + +export const testLessons = [ + { + _id: "67e605c6fb8fbc9c9bbb6d81", + title: "Test Lesson", + type: "Lesson", + source: "https://test.com/lesson.pdf", + pageIndex: 0, + }, +]; + +export const testActivities = [ + { + _id: "67e60610fb8fbc9c9bbb6d82", + title: "Test Activity 1", + type: "Activity", + layout: [], + pageIndex: 0, + }, + { + _id: "67e60617fb8fbc9c9bbb6d83", + title: "Test Activity 2", + type: "Activity", + layout: [], + pageIndex: 1, + }, +]; + +export const testCourseModules = [ + { + _id: "67e60622fb8fbc9c9bbb6d84", + displayIndex: 0, + title: "Test Course Module", + pages: testActivities.map(activity => activity._id), + }, +]; + +export const testCourseUnits = [ + { + _id: "67e6062cfb8fbc9c9bbb6d85", + displayIndex: 0, + title: "Test Course Unit", + modules: testCourseModules.map(module => module._id), + }, +]; + +export const testAdmins = [ + { + firstName: "Peter", + lastName: "Pan", + _id: "67e6062ffb8fbc9c9bbb6d86", + authId: "67e6062ffb8fbc9c9bbb6d87", + email: "peter.pan@example.com", + status: "Active" as Status, + role: "Administrator" as Role, + }, +]; + +export const testFacilitators = [ + { + firstName: "Wendy", + lastName: "Darling", + _id: "67e60630fb8fbc9c9bbb6d88", + authId: "67e60631fb8fbc9c9bbb6d89", + email: "wendy.darling@example.com", + status: "Active" as Status, + role: "Facilitator" as Role, + learners: ["67e60671fb8fbc9c9bbb6d8a"], + }, +]; + +export const testLearners = [ + { + firstName: "John", + lastName: "Doe", + _id: "67e60671fb8fbc9c9bbb6d8a", + authId: "67e6068afb8fbc9c9bbb6d8b", + email: "john.doe@example.com", + status: "Active" as Status, + role: "Learner" as Role, + facilitator: testFacilitators[0]._id, + activitiesCompleted: new Map([ + [testCourseUnits[0]._id.toString(), new Map([ + [testCourseModules[0]._id.toString(), testCourseModules[0].pages.map(page => page.toString())], + ])] + ]), + }, +]; + +export const testLearnersDTO: LearnerDTO[] = [ + { + firstName: "John", + lastName: "Doe", + id: "67e60671fb8fbc9c9bbb6d8a", + email: "john.doe@example.com", + status: "Active" as Status, + role: "Learner" as Role, + facilitator: testFacilitators[0]._id, + activitiesCompleted: new Map([ + [testCourseUnits[0]._id.toString(), new Map([ + [testCourseModules[0]._id.toString(), testCourseModules[0].pages.map(page => page.toString())], + ])] + ]), + }, +]; + +export const testUsers = [...testAdmins, ...testFacilitators, ...testLearners]; diff --git a/backend/package.json b/backend/package.json index efd0f633..463956fe 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,7 +26,7 @@ "dotenv": "^16.5.0", "express": "^4.19.2", "express-rate-limit": "^6.2.0", - "firebase-admin": "^11.10.0", + "firebase-admin": "^13.2.0", "generate-password": "^1.7.1", "json2csv": "^5.0.6", "lodash": "^4.17.21", diff --git a/backend/services/implementations/__tests__/courseModuleService.test.ts b/backend/services/implementations/__tests__/courseModuleService.test.ts index 74838f23..28c8249c 100644 --- a/backend/services/implementations/__tests__/courseModuleService.test.ts +++ b/backend/services/implementations/__tests__/courseModuleService.test.ts @@ -1,6 +1,5 @@ -import { ObjectId } from "mongodb"; -import CourseModuleService from "../courseModuleService"; import db from "../../../testUtils/testDb"; +import CourseModuleService from "../courseModuleService"; describe("courseModuleService", (): void => { let courseModuleService: CourseModuleService; @@ -21,12 +20,7 @@ describe("courseModuleService", (): void => { await db.clear(); }); - it("uploadLessons", async () => { - const res = await courseModuleService.uploadLessons( - new ObjectId().toHexString(), - "uploads/", - ); - // eslint-disable-next-line no-console - console.log("Result:", res); - }, 20_000); + it("placeholder", async () => { + expect(true).toBe(true); + }); }); diff --git a/backend/services/implementations/__tests__/userService.test.ts b/backend/services/implementations/__tests__/userService.test.ts index b2f54b9e..397bbbf8 100644 --- a/backend/services/implementations/__tests__/userService.test.ts +++ b/backend/services/implementations/__tests__/userService.test.ts @@ -3,28 +3,35 @@ import UserService from "../userService"; import { UserDTO } from "../../../types/userTypes"; +import { testActivities, testCourseModules, testCourseUnits, testLearnersDTO, testLessons, testUsers } from "../../../__mocks__/mockData"; +import coursemoduleMgmodel from "../../../models/coursemodule.mgmodel"; +import coursepageMgmodel from "../../../models/coursepage.mgmodel"; +import courseunitMgmodel from "../../../models/courseunit.mgmodel"; import db from "../../../testUtils/testDb"; -const testUsers = [ - { - firstName: "Peter", - lastName: "Pan", - authId: "123", - role: "Administrator", - }, - { - firstName: "Wendy", - lastName: "Darling", - authId: "321", - role: "Facilitator", - }, -]; - jest.mock("firebase-admin", () => { const auth = jest.fn().mockReturnValue({ getUser: jest.fn().mockReturnValue({ email: "test@test.com" }), }); - return { auth }; + + const storage = jest.fn().mockReturnValue({ + bucket: jest.fn().mockReturnValue({ + file: jest.fn().mockReturnValue({ + save: jest.fn(), + exists: jest.fn().mockReturnValue([true]), + delete: jest.fn(), + }), + upload: jest.fn(), + }), + }); + + return { auth, storage }; +}); + +jest.mock("firebase-admin/storage", () => { + return { + getDownloadURL: jest.fn().mockReturnValue("https://test.com/image.jpg") + }; }); describe("mongo userService", (): void => { @@ -40,6 +47,10 @@ describe("mongo userService", (): void => { beforeEach(async () => { userService = new UserService(); + await MgUser.insertMany(testUsers); + await courseunitMgmodel.insertMany(testCourseUnits); + await coursemoduleMgmodel.insertMany(testCourseModules); + await coursepageMgmodel.insertMany([...testLessons, ...testActivities]); }); afterEach(async () => { @@ -47,8 +58,6 @@ describe("mongo userService", (): void => { }); it("getUsers", async () => { - await MgUser.insertMany(testUsers); - const res = await userService.getUsers(); res.forEach((user: UserDTO, i) => { @@ -57,4 +66,11 @@ describe("mongo userService", (): void => { expect(user.role).toEqual(testUsers[i].role); }); }); + + it("getNumCompletedModules", async () => { + const testUser = testLearnersDTO[0]; + const res = await userService.getNumCompletedModules(testUser); + + expect(res).toEqual(1); + }); }); diff --git a/backend/services/implementations/fileStorageService.ts b/backend/services/implementations/fileStorageService.ts index de784afc..236f9162 100644 --- a/backend/services/implementations/fileStorageService.ts +++ b/backend/services/implementations/fileStorageService.ts @@ -8,7 +8,7 @@ import IFileStorageService from "../interfaces/fileStorageService"; const Logger = logger(__filename); class FileStorageService implements IFileStorageService { - bucketName: string; + public readonly bucketName: string; constructor(bucketName: string) { this.bucketName = bucketName; @@ -34,7 +34,7 @@ class FileStorageService implements IFileStorageService { async createFile( fileName: string, filePath: string, - contentType: string | null = null, + contentType?: string, allowOverwrite = false, ): Promise { try { @@ -45,7 +45,7 @@ class FileStorageService implements IFileStorageService { } await bucket.upload(filePath, { destination: fileName, - metadata: { contentType }, + metadata: contentType ? { contentType } : undefined, }); } catch (error: unknown) { Logger.error(`Failed to upload file. Reason = ${getErrorMessage(error)}`); @@ -56,7 +56,7 @@ class FileStorageService implements IFileStorageService { async updateFile( fileName: string, filePath: string, - contentType: string | null = null, + contentType?: string, ): Promise { try { const bucket = storage().bucket(this.bucketName); @@ -66,7 +66,7 @@ class FileStorageService implements IFileStorageService { } await bucket.upload(filePath, { destination: fileName, - metadata: { contentType }, + metadata: contentType ? { contentType } : undefined, }); } catch (error: unknown) { Logger.error(`Failed to update file. Reason = ${getErrorMessage(error)}`); @@ -91,13 +91,13 @@ class FileStorageService implements IFileStorageService { async uploadImage( fileName: string, fileData: Buffer | null = null, - contentType: string | null = null, + contentType?: string, ): Promise { try { const bucket = storage().bucket(this.bucketName); if (fileData) { await bucket.file(fileName).save(fileData, { - metadata: { contentType }, + metadata: contentType ? { contentType } : undefined, }); } return await this.getFile(fileName); diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index d06fd5bb..36c3a4df 100644 --- a/backend/services/implementations/userService.ts +++ b/backend/services/implementations/userService.ts @@ -364,17 +364,20 @@ class UserService implements IUserService { } async getNumCompletedModules(learner: LearnerDTO): Promise { - const numCompletedModules = Array.from( - learner.activitiesCompleted.values(), - ).reduce((acc, curr) => { - const completedModules = Array.from(curr.entries()).filter( - async ([moduleId, activitiesCompletedInModule]) => { + let numCompletedModules = 0; + + for (const unitsMap of learner.activitiesCompleted.values()) { + for (const [moduleId, activitiesCompletedInModule] of unitsMap.entries()) { + try { const module = await courseModuleService.getCourseModule(moduleId); - return activitiesCompletedInModule.length === module?.pages.length; - }, - ); - return acc + completedModules.length; - }, 0); + if (module && activitiesCompletedInModule.length === module.pages.length) { + numCompletedModules += 1; + } + } catch (error) { + console.error(`Error finding module ${moduleId}:`, error); + } + } + } return numCompletedModules; } diff --git a/backend/yarn.lock b/backend/yarn.lock index 302c2755..9f802a58 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -572,21 +572,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - "@babel/helper-validator-option@^7.24.8": version "7.24.8" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" @@ -617,13 +607,6 @@ dependencies: "@babel/types" "^7.25.6" -"@babel/parser@^7.20.15": - version "7.26.8" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.8.tgz#deca2b4d99e5e1b1553843b99823f118da6107c2" - integrity sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw== - dependencies: - "@babel/types" "^7.26.8" - "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -767,14 +750,6 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" -"@babel/types@^7.26.8": - version "7.26.8" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.8.tgz#97dcdc190fab45be7f3dc073e3c11160d677c127" - integrity sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -833,138 +808,140 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@fastify/busboy@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-1.2.1.tgz#9c6db24a55f8b803b5222753b24fe3aea2ba9ca3" - integrity sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q== - dependencies: - text-decoding "^1.0.0" +"@fastify/busboy@^3.0.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-3.1.1.tgz#af3aea7f1e52ec916d8b5c9dcc0f09d4c060a3fc" + integrity sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw== -"@firebase/app-types@0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.0.tgz#35b5c568341e9e263b29b3d2ba0e9cfc9ec7f01e" - integrity sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q== +"@firebase/app-check-interop-types@0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz#ed9c4a4f48d1395ef378f007476db3940aa5351a" + integrity sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A== -"@firebase/auth-interop-types@0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz#78884f24fa539e34a06c03612c75f222fcc33742" - integrity sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg== +"@firebase/app-types@0.9.3": + version "0.9.3" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.3.tgz#8408219eae9b1fb74f86c24e7150a148460414ad" + integrity sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw== -"@firebase/component@0.6.4": - version "0.6.4" - resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.4.tgz#8981a6818bd730a7554aa5e0516ffc9b1ae3f33d" - integrity sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA== - dependencies: - "@firebase/util" "1.9.3" - tslib "^2.1.0" +"@firebase/auth-interop-types@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz#176a08686b0685596ff03d7879b7e4115af53de0" + integrity sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA== -"@firebase/database-compat@^0.3.4": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-0.3.4.tgz#4e57932f7a5ba761cd5ac946ab6b6ab3f660522c" - integrity sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg== +"@firebase/component@0.6.13": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.13.tgz#6513379f1c09264133969d87282ce0d5bbbb2cd9" + integrity sha512-I/Eg1NpAtZ8AAfq8mpdfXnuUpcLxIDdCDtTzWSh+FXnp/9eCKJ3SNbOCKrUCyhLzNa2SiPJYruei0sxVjaOTeg== dependencies: - "@firebase/component" "0.6.4" - "@firebase/database" "0.14.4" - "@firebase/database-types" "0.10.4" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.3" + "@firebase/util" "1.11.0" tslib "^2.1.0" -"@firebase/database-types@0.10.4", "@firebase/database-types@^0.10.4": - version "0.10.4" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.10.4.tgz#47ba81113512dab637abace61cfb65f63d645ca7" - integrity sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ== - dependencies: - "@firebase/app-types" "0.9.0" - "@firebase/util" "1.9.3" +"@firebase/database-compat@^2.0.0": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-2.0.5.tgz#110f612901995f9800f2435f58686e0c6f3d2544" + integrity sha512-CNf1UbvWh6qIaSf4sn6sx2DTDz/em/D7QxULH1LTxxDQHr9+CeYGvlAqrKnk4ZH0P0eIHyQFQU7RwkUJI0B9gQ== + dependencies: + "@firebase/component" "0.6.13" + "@firebase/database" "1.0.14" + "@firebase/database-types" "1.0.10" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" + tslib "^2.1.0" -"@firebase/database@0.14.4": - version "0.14.4" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.14.4.tgz#9e7435a16a540ddfdeb5d99d45618e6ede179aa6" - integrity sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ== - dependencies: - "@firebase/auth-interop-types" "0.2.1" - "@firebase/component" "0.6.4" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.3" +"@firebase/database-types@1.0.10", "@firebase/database-types@^1.0.6": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.10.tgz#14cfed45bb06394cf1641e19265cbf90e4f6fb51" + integrity sha512-mH6RC1E9/Pv8jf1/p+M8YFTX+iu+iHDN89hecvyO7wHrI4R1V0TXjxOHvX3nLJN1sfh0CWG6CHZ0VlrSmK/cwg== + dependencies: + "@firebase/app-types" "0.9.3" + "@firebase/util" "1.11.0" + +"@firebase/database@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.14.tgz#1d579b345c0f926eaddb7703051999489300c3bd" + integrity sha512-9nxYtkHAG02/Nh2Ssms1T4BbWPPjiwohCvkHDUl4hNxnki1kPgsLo5xe9kXNzbacOStmVys+RUXvwzynQSKmUQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.13" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.0" faye-websocket "0.11.4" tslib "^2.1.0" -"@firebase/logger@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.0.tgz#15ecc03c452525f9d47318ad9491b81d1810f113" - integrity sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA== +"@firebase/logger@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.4.tgz#29e8379d20fd1149349a195ee6deee4573a86f48" + integrity sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g== dependencies: tslib "^2.1.0" -"@firebase/util@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.3.tgz#45458dd5cd02d90e55c656e84adf6f3decf4b7ed" - integrity sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA== +"@firebase/util@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.11.0.tgz#e74ee2dc260ec4f9e75fe5d52bc4b0254d9872a9" + integrity sha512-PzSrhIr++KI6y4P6C/IdgBNMkEx0Ex6554/cYd0Hm+ovyFSJtJXqb/3OSIdnBoa2cpwZT1/GW56EmRc5qEc5fQ== dependencies: tslib "^2.1.0" -"@google-cloud/firestore@^6.8.0": - version "6.8.0" - resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-6.8.0.tgz#d8c852844c381afaf62592796606c10e178400b5" - integrity sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA== +"@google-cloud/firestore@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-7.11.0.tgz#a8a2b61d5069ba23ff2a1b98eac78d15a904a40f" + integrity sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA== dependencies: + "@opentelemetry/api" "^1.3.0" fast-deep-equal "^3.1.1" functional-red-black-tree "^1.0.1" - google-gax "^3.5.7" - protobufjs "^7.2.5" + google-gax "^4.3.3" + protobufjs "^7.2.6" -"@google-cloud/paginator@^3.0.7": - version "3.0.7" - resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" - integrity sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ== +"@google-cloud/paginator@^5.0.0": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-5.0.2.tgz#86ad773266ce9f3b82955a8f75e22cd012ccc889" + integrity sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg== dependencies: arrify "^2.0.0" extend "^3.0.2" -"@google-cloud/projectify@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-3.0.0.tgz#302b25f55f674854dce65c2532d98919b118a408" - integrity sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA== +"@google-cloud/projectify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-4.0.0.tgz#d600e0433daf51b88c1fa95ac7f02e38e80a07be" + integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== -"@google-cloud/promisify@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-3.0.1.tgz#8d724fb280f47d1ff99953aee0c1669b25238c2e" - integrity sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA== +"@google-cloud/promisify@^4.0.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-4.1.0.tgz#df8b060f0121c6462233f5420738dcda09c6df4a" + integrity sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg== -"@google-cloud/storage@^6.9.5": - version "6.12.0" - resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-6.12.0.tgz#a5d3093cc075252dca5bd19a3cfda406ad3a9de1" - integrity sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw== +"@google-cloud/storage@^7.14.0": + version "7.15.2" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.15.2.tgz#5371767d78064d4fec259594520be38f5f02ab06" + integrity sha512-+2k+mcQBb9zkaXMllf2wwR/rI07guAx+eZLWsGTDihW2lJRGfiqB7xu1r7/s4uvSP/T+nAumvzT5TTscwHKJ9A== dependencies: - "@google-cloud/paginator" "^3.0.7" - "@google-cloud/projectify" "^3.0.0" - "@google-cloud/promisify" "^3.0.0" + "@google-cloud/paginator" "^5.0.0" + "@google-cloud/projectify" "^4.0.0" + "@google-cloud/promisify" "^4.0.0" abort-controller "^3.0.0" async-retry "^1.3.3" - compressible "^2.0.12" - duplexify "^4.0.0" - ent "^2.2.0" - extend "^3.0.2" - fast-xml-parser "^4.2.2" - gaxios "^5.0.0" - google-auth-library "^8.0.1" + duplexify "^4.1.3" + fast-xml-parser "^4.4.1" + gaxios "^6.0.2" + google-auth-library "^9.6.3" + html-entities "^2.5.2" mime "^3.0.0" - mime-types "^2.0.8" p-limit "^3.0.1" - retry-request "^5.0.0" - teeny-request "^8.0.0" + retry-request "^7.0.0" + teeny-request "^9.0.0" uuid "^8.0.0" -"@grpc/grpc-js@~1.8.0": - version "1.8.22" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.22.tgz#847930c9af46e14df05b57fc12325db140ceff1d" - integrity sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA== +"@grpc/grpc-js@^1.10.9": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.13.2.tgz#376543c23eedc03ea019ff37050dc0b0936bfe8f" + integrity sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g== dependencies: - "@grpc/proto-loader" "^0.7.0" - "@types/node" ">=12.12.47" + "@grpc/proto-loader" "^0.7.13" + "@js-sdsl/ordered-map" "^4.4.2" -"@grpc/proto-loader@^0.7.0": +"@grpc/proto-loader@^0.7.13": version "0.7.13" resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== @@ -1229,12 +1206,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@jsdoc/salty@^0.2.1": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.9.tgz#4d8c147f7ca011532681ce86352a77a0178f1dec" - integrity sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw== - dependencies: - lodash "^4.17.21" +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== "@mongodb-js/saslprep@^1.1.0", "@mongodb-js/saslprep@^1.1.9": version "1.1.9" @@ -1264,6 +1239,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@opentelemetry/api@^1.3.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== + "@pdf-lib/standard-fonts@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz#8ba691c4421f71662ed07c9a0294b44528af2d7f" @@ -1861,6 +1841,11 @@ "@types/connect" "*" "@types/node" "*" +"@types/caseless@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" + integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== + "@types/connect@*": version "3.4.38" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" @@ -1911,7 +1896,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@*", "@types/express@^4.17.11", "@types/express@^4.17.17": +"@types/express@*", "@types/express@^4.17.11", "@types/express@^4.17.20": version "4.17.21" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== @@ -1921,14 +1906,6 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/glob@*": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" - integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w== - dependencies: - "@types/minimatch" "^5.1.2" - "@types/node" "*" - "@types/graceful-fs@^4.1.2": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" @@ -1980,19 +1957,14 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonwebtoken@^9.0.2": - version "9.0.8" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.8.tgz#313490052801edfb031bb32b6bbd77cc9f230852" - integrity sha512-7fx54m60nLFUVYlxAB1xpe9CBWX2vSrk50Y6ogRJ1v5xxtba7qXTg5BgYDN5dq+yuQQ9HaVlHJyAAt1/mxryFg== +"@types/jsonwebtoken@^9.0.4": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3" + integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ== dependencies: "@types/ms" "*" "@types/node" "*" -"@types/linkify-it@^5": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" - integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== - "@types/lodash@*", "@types/lodash@^4.14.168": version "4.17.9" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.9.tgz#0dc4902c229f6b8e2ac5456522104d7b1a230290" @@ -2003,29 +1975,11 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== -"@types/markdown-it@^14.1.1": - version "14.1.2" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" - integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== - dependencies: - "@types/linkify-it" "^5" - "@types/mdurl" "^2" - -"@types/mdurl@^2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" - integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== - "@types/mime@^1": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== -"@types/minimatch@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" - integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== - "@types/mongoose-lean-id@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/mongoose-lean-id/-/mongoose-lean-id-1.0.0.tgz#1ded4d83f9fc55b5e600b6ec2dcf6344d049e915" @@ -2061,7 +2015,7 @@ "@types/node" "*" form-data "^4.0.0" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0": +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.7.0": version "22.6.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.6.1.tgz#e531a45f4d78f14a8468cb9cdc29dc9602afc7ac" integrity sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw== @@ -2073,6 +2027,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== +"@types/node@^22.8.7": + version "22.13.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.14.tgz#70d84ec91013dcd2ba2de35532a5a14c2b4cc912" + integrity sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w== + dependencies: + undici-types "~6.20.0" + "@types/nodemailer@^6.4.1": version "6.4.16" resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.16.tgz#db006abcb1e1c8e6ea2fb53b27fefec3c03eaa6c" @@ -2104,13 +2065,15 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/rimraf@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8" - integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ== +"@types/request@^2.48.8": + version "2.48.12" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30" + integrity sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw== dependencies: - "@types/glob" "*" + "@types/caseless" "*" "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" "@types/send@*": version "0.17.4" @@ -2157,6 +2120,11 @@ resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.6.tgz#d785ee90c52d7cc020e249c948c36f7b32d1e217" integrity sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA== +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + "@types/triple-beam@^1.3.2": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" @@ -2417,6 +2385,11 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + ajv-draft-04@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" @@ -2738,11 +2711,6 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - body-parser@1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -2874,6 +2842,14 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -2905,13 +2881,6 @@ caniuse-lite@^1.0.30001646: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz#1529a723505e429fdfd49532e9fc42273ba7fed7" integrity sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA== -catharsis@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" - integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== - dependencies: - lodash "^4.17.15" - chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -3052,13 +3021,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== -compressible@^2.0.12: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -3240,7 +3202,7 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== -deep-is@^0.1.3, deep-is@~0.1.3: +deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -3346,7 +3308,16 @@ dotenv@^16.5.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.5.0.tgz#092b49f25f808f020050051d1ff258e404c78692" integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg== -duplexify@^4.0.0: +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +duplexify@^4.0.0, duplexify@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== @@ -3431,18 +3402,6 @@ engine.io@~6.6.0: engine.io-parser "~5.2.1" ws "~8.17.1" -ent@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" - integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== - dependencies: - punycode "^1.4.1" - -entities@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3509,6 +3468,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -3521,6 +3485,13 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" +es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" @@ -3530,6 +3501,16 @@ es-set-tostringtag@^2.0.3: has-tostringtag "^1.0.2" hasown "^2.0.1" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" @@ -3571,18 +3552,6 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -escodegen@^1.13.0: - version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - escodegen@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" @@ -3735,7 +3704,7 @@ eslint@^8.56.0: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -3763,11 +3732,6 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - estraverse@^5.1.0, estraverse@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" @@ -3865,6 +3829,11 @@ extend@^3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +farmhash-modern@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/farmhash-modern/-/farmhash-modern-1.1.0.tgz#c36b34ad196290d57b0b482dc89e637d0b59835f" + integrity sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3891,16 +3860,11 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" - integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== - fast-uri@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" @@ -3913,12 +3877,12 @@ fast-xml-parser@4.4.1: dependencies: strnum "^1.0.5" -fast-xml-parser@^4.2.2: - version "4.5.1" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz#a7e665ff79b7919100a5202f23984b6150f9b31e" - integrity sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w== +fast-xml-parser@^4.4.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz#c54d6b35aa0f23dc1ea60b6c884340c006dc6efb" + integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig== dependencies: - strnum "^1.0.5" + strnum "^1.1.1" fastq@^1.6.0: version "1.17.1" @@ -4010,22 +3974,24 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -firebase-admin@^11.10.0: - version "11.11.1" - resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-11.11.1.tgz#6712923de70d218c9f514d73005d976d03339605" - integrity sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg== - dependencies: - "@fastify/busboy" "^1.2.1" - "@firebase/database-compat" "^0.3.4" - "@firebase/database-types" "^0.10.4" - "@types/node" ">=12.12.47" +firebase-admin@^13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-13.2.0.tgz#93c895b3f7bdef556d3b0f00017aff79ac86d0f1" + integrity sha512-qQBTKo0QWCDaWwISry989pr8YfZSSk00rNCKaucjOgltEm3cCYzEe4rODqBd1uUwma+Iu5jtAzg89Nfsjr3fGg== + dependencies: + "@fastify/busboy" "^3.0.0" + "@firebase/database-compat" "^2.0.0" + "@firebase/database-types" "^1.0.6" + "@types/node" "^22.8.7" + farmhash-modern "^1.1.0" + google-auth-library "^9.14.2" jsonwebtoken "^9.0.0" - jwks-rsa "^3.0.1" + jwks-rsa "^3.1.0" node-forge "^1.3.1" - uuid "^9.0.0" + uuid "^11.0.2" optionalDependencies: - "@google-cloud/firestore" "^6.8.0" - "@google-cloud/storage" "^6.9.5" + "@google-cloud/firestore" "^7.11.0" + "@google-cloud/storage" "^7.14.0" flat-cache@^3.0.4: version "3.2.0" @@ -4053,6 +4019,17 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data@^2.5.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.3.tgz#f9bcf87418ce748513c0c3494bb48ec270c97acc" + integrity sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + mime-types "^2.1.35" + safe-buffer "^5.2.1" + form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -4135,22 +4112,24 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -gaxios@^5.0.0, gaxios@^5.0.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.1.3.tgz#f7fa92da0fe197c846441e5ead2573d4979e9013" - integrity sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA== +gaxios@^6.0.0, gaxios@^6.0.2, gaxios@^6.1.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== dependencies: extend "^3.0.2" - https-proxy-agent "^5.0.0" + https-proxy-agent "^7.0.1" is-stream "^2.0.0" node-fetch "^2.6.9" + uuid "^9.0.1" -gcp-metadata@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.3.0.tgz#6f45eb473d0cb47d15001476b48b663744d25408" - integrity sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w== +gcp-metadata@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494" + integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A== dependencies: - gaxios "^5.0.0" + gaxios "^6.1.1" + google-logging-utils "^0.0.2" json-bigint "^1.0.0" generate-password@^1.7.1: @@ -4179,6 +4158,22 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -4189,6 +4184,14 @@ get-port@^5.1.1: resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -4229,17 +4232,6 @@ glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -4272,48 +4264,40 @@ globby@^11.0.3, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -google-auth-library@^8.0.1, google-auth-library@^8.0.2: - version "8.9.0" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-8.9.0.tgz#15a271eb2ec35d43b81deb72211bd61b1ef14dd0" - integrity sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg== +google-auth-library@^9.14.2, google-auth-library@^9.3.0, google-auth-library@^9.6.3: + version "9.15.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" + integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== dependencies: - arrify "^2.0.0" base64-js "^1.3.0" ecdsa-sig-formatter "^1.0.11" - fast-text-encoding "^1.0.0" - gaxios "^5.0.0" - gcp-metadata "^5.3.0" - gtoken "^6.1.0" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" jws "^4.0.0" - lru-cache "^6.0.0" -google-gax@^3.5.7: - version "3.6.1" - resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-3.6.1.tgz#02c78fc496f5adf86f2ca9145545f4b6575f6118" - integrity sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w== +google-gax@^4.3.3: + version "4.4.1" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-4.4.1.tgz#95a9cf7ee7777ac22d1926a45b5f886dd8beecae" + integrity sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg== dependencies: - "@grpc/grpc-js" "~1.8.0" - "@grpc/proto-loader" "^0.7.0" + "@grpc/grpc-js" "^1.10.9" + "@grpc/proto-loader" "^0.7.13" "@types/long" "^4.0.0" - "@types/rimraf" "^3.0.2" abort-controller "^3.0.0" duplexify "^4.0.0" - fast-text-encoding "^1.0.3" - google-auth-library "^8.0.2" - is-stream-ended "^0.1.4" - node-fetch "^2.6.1" + google-auth-library "^9.3.0" + node-fetch "^2.7.0" object-hash "^3.0.0" - proto3-json-serializer "^1.0.0" - protobufjs "7.2.4" - protobufjs-cli "1.1.1" - retry-request "^5.0.0" + proto3-json-serializer "^2.0.2" + protobufjs "^7.3.2" + retry-request "^7.0.0" + uuid "^9.0.1" -google-p12-pem@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-4.0.1.tgz#82841798253c65b7dc2a4e5fe9df141db670172a" - integrity sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ== - dependencies: - node-forge "^1.3.1" +google-logging-utils@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a" + integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== gopd@^1.0.1: version "1.0.1" @@ -4322,7 +4306,12 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.9: +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4332,13 +4321,12 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -gtoken@^6.1.0: - version "6.1.2" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-6.1.2.tgz#aeb7bdb019ff4c3ba3ac100bbe7b6e74dce0e8bc" - integrity sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ== +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== dependencies: - gaxios "^5.0.1" - google-p12-pem "^4.0.0" + gaxios "^6.0.0" jws "^4.0.0" has-bigints@^1.0.1, has-bigints@^1.0.2: @@ -4373,6 +4361,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -4394,6 +4387,11 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-entities@^2.5.2: + version "2.5.3" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.3.tgz#d8a0680bd24ee35af8c74d6d4b695627dde61c00" + integrity sha512-D3AfvN7SjhTgBSA8L1BN4FpPzuEd06uy4lHwSoRWr0lndi9BKaNzPLKGOWZ2ocSGguozr08TTb2jhCLHaemruw== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -4441,6 +4439,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.1: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -4659,11 +4665,6 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" -is-stream-ended@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" - integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -5184,7 +5185,7 @@ jju@~1.4.0: resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== -jose@^4.14.6: +jose@^4.15.4: version "4.15.9" resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100" integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== @@ -5209,39 +5210,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js2xmlparser@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" - integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== - dependencies: - xmlcreate "^2.0.4" - jsbn@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== -jsdoc@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-4.0.4.tgz#86565a9e39cc723a3640465b3fb189a22d1206ca" - integrity sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw== - dependencies: - "@babel/parser" "^7.20.15" - "@jsdoc/salty" "^0.2.1" - "@types/markdown-it" "^14.1.1" - bluebird "^3.7.2" - catharsis "^0.9.0" - escape-string-regexp "^2.0.0" - js2xmlparser "^4.0.2" - klaw "^3.0.0" - markdown-it "^14.1.0" - markdown-it-anchor "^8.6.7" - marked "^4.0.10" - mkdirp "^1.0.4" - requizzle "^0.2.3" - strip-json-comments "^3.1.0" - underscore "~1.13.2" - jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -5379,15 +5352,15 @@ jwa@^2.0.0: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jwks-rsa@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.1.0.tgz#50406f23e38c9b2682cd437f824d7d61aa983171" - integrity sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg== +jwks-rsa@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.2.0.tgz#132bc8bfa7b03928a273bbc93486f70226449e04" + integrity sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww== dependencies: - "@types/express" "^4.17.17" - "@types/jsonwebtoken" "^9.0.2" + "@types/express" "^4.17.20" + "@types/jsonwebtoken" "^9.0.4" debug "^4.3.4" - jose "^4.14.6" + jose "^4.15.4" limiter "^1.1.5" lru-memoizer "^2.2.0" @@ -5419,13 +5392,6 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" -klaw@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" - integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== - dependencies: - graceful-fs "^4.1.9" - kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -5449,14 +5415,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - limiter@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" @@ -5467,13 +5425,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-it@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" - integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== - dependencies: - uc.micro "^2.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -5548,7 +5499,7 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash@^4.17.15, lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5618,38 +5569,16 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -markdown-it-anchor@^8.6.7: - version "8.6.7" - resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" - integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== - -markdown-it@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" - integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== - dependencies: - argparse "^2.0.1" - entities "^4.4.0" - linkify-it "^5.0.0" - mdurl "^2.0.0" - punycode.js "^2.3.1" - uc.micro "^2.1.0" - -marked@^4.0.10: - version "4.3.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" - integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== md5-file@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-5.0.0.tgz#e519f631feca9c39e7f9ea1780b63c4745012e20" integrity sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw== -mdurl@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" - integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5693,12 +5622,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -"mime-db@>= 1.43.0 < 2": - version "1.53.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" - integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== - -mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -5727,13 +5651,6 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" @@ -5924,7 +5841,7 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -node-fetch@^2.6.1, node-fetch@^2.6.9: +node-fetch@^2.6.1, node-fetch@^2.6.9, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -6086,18 +6003,6 @@ optional-require@^1.1.8: dependencies: require-at "^1.0.6" -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -6335,11 +6240,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== - prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -6384,48 +6284,14 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -proto3-json-serializer@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz#1b5703152b6ce811c5cdcc6468032caf53521331" - integrity sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw== - dependencies: - protobufjs "^7.0.0" - -protobufjs-cli@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz#f531201b1c8c7772066aa822bf9a08318b24a704" - integrity sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA== - dependencies: - chalk "^4.0.0" - escodegen "^1.13.0" - espree "^9.0.0" - estraverse "^5.1.0" - glob "^8.0.0" - jsdoc "^4.0.0" - minimist "^1.2.0" - semver "^7.1.2" - tmp "^0.2.1" - uglify-js "^3.7.7" - -protobufjs@7.2.4: - version "7.2.4" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.4.tgz#3fc1ec0cdc89dd91aef9ba6037ba07408485c3ae" - integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== +proto3-json-serializer@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz#5b705203b4d58f3880596c95fad64902617529dd" + integrity sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ== dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" + protobufjs "^7.2.5" -protobufjs@^7.0.0, protobufjs@^7.2.5: +protobufjs@^7.2.5, protobufjs@^7.2.6, protobufjs@^7.3.2: version "7.4.0" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== @@ -6461,16 +6327,6 @@ pstree.remy@^1.1.8: resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== -punycode.js@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" - integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -6577,13 +6433,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -requizzle@^0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" - integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== - dependencies: - lodash "^4.17.21" - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -6615,13 +6464,14 @@ resolve@^1.20.0, resolve@^1.22.4, resolve@~1.22.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -retry-request@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-5.0.2.tgz#143d85f90c755af407fcc46b7166a4ba520e44da" - integrity sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ== +retry-request@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-7.0.2.tgz#60bf48cfb424ec01b03fca6665dee91d06dd95f3" + integrity sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w== dependencies: - debug "^4.1.1" + "@types/request" "^2.48.8" extend "^3.0.2" + teeny-request "^9.0.0" retry@0.13.1: version "0.13.1" @@ -6657,7 +6507,7 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -6715,7 +6565,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.1.2, semver@^7.5.4: +semver@^7.5.4: version "7.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== @@ -7046,7 +6896,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -7056,6 +6906,11 @@ strnum@^1.0.5: resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== +strnum@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4" + integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== + stubs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" @@ -7123,14 +6978,14 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -teeny-request@^8.0.0: - version "8.0.3" - resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-8.0.3.tgz#5cb9c471ef5e59f2fca8280dc3c5909595e6ca24" - integrity sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww== +teeny-request@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-9.0.0.tgz#18140de2eb6595771b1b02203312dfad79a4716d" + integrity sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g== dependencies: http-proxy-agent "^5.0.0" https-proxy-agent "^5.0.0" - node-fetch "^2.6.1" + node-fetch "^2.6.9" stream-events "^1.0.5" uuid "^9.0.0" @@ -7151,11 +7006,6 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-decoding@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/text-decoding/-/text-decoding-1.0.0.tgz#38a5692d23b5c2b12942d6e245599cb58b1bc52f" - integrity sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA== - text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -7316,13 +7166,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== - dependencies: - prelude-ls "~1.1.2" - type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -7412,16 +7255,6 @@ typescript@^4.1.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -uc.micro@^2.0.0, uc.micro@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" - integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== - -uglify-js@^3.7.7: - version "3.19.3" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" - integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== - umzug@^3.0.0-beta.16: version "3.8.2" resolved "https://registry.yarnpkg.com/umzug/-/umzug-3.8.2.tgz#53c2189604d36956d7b75a89128108d0e3073a9f" @@ -7448,16 +7281,16 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -underscore@~1.13.2: - version "1.13.7" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" - integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== - undici-types@~6.19.2: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -7506,6 +7339,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^11.0.2: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^8.0.0, uuid@^8.3.1, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -7690,7 +7528,7 @@ winston@^3.3.3: triple-beam "^1.3.0" winston-transport "^4.7.0" -word-wrap@^1.2.5, word-wrap@~1.2.3: +word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== @@ -7739,11 +7577,6 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmlcreate@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" - integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== - xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From e824392405f380a2f86a11a5d26971578d3e627a Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Mon, 31 Mar 2025 23:31:45 -0400 Subject: [PATCH 04/12] Fix markActivityCompleted endpoint :face_exhaling: --- backend/__mocks__/mockData.ts | 11 ++++++----- backend/rest/authRoutes.ts | 12 +++++++----- backend/rest/userRoutes.ts | 10 +++++----- .../__tests__/userService.test.ts | 16 ++++++++++++++-- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/backend/__mocks__/mockData.ts b/backend/__mocks__/mockData.ts index 0cc783f0..e3ec197a 100644 --- a/backend/__mocks__/mockData.ts +++ b/backend/__mocks__/mockData.ts @@ -80,11 +80,12 @@ export const testLearners = [ status: "Active" as Status, role: "Learner" as Role, facilitator: testFacilitators[0]._id, - activitiesCompleted: new Map([ - [testCourseUnits[0]._id.toString(), new Map([ - [testCourseModules[0]._id.toString(), testCourseModules[0].pages.map(page => page.toString())], - ])] - ]), + activitiesCompleted: { + [testCourseUnits[0]._id.toString()]: { + [testCourseModules[0]._id.toString()]: [], + // testCourseModules[0].pages.map(page => page.toString()) + } + } }, ]; diff --git a/backend/rest/authRoutes.ts b/backend/rest/authRoutes.ts index 46d4fabc..a8836d36 100644 --- a/backend/rest/authRoutes.ts +++ b/backend/rest/authRoutes.ts @@ -3,15 +3,15 @@ import { CookieOptions, Router } from "express"; import { generate } from "generate-password"; import { getAccessToken, - isAuthorizedByUserId, isAuthorizedByRole, + isAuthorizedByUserId, isFirstTimeInvitedUser, } from "../middlewares/auth"; import { + forgotPasswordRequestValidator, + inviteUserRequestValidator, loginRequestValidator, signupRequestValidator, - inviteUserRequestValidator, - forgotPasswordRequestValidator, updateTemporaryPasswordRequestValidator, updateUserStatusRequestValidator, } from "../middlewares/validators/authValidators"; @@ -22,8 +22,8 @@ import UserService from "../services/implementations/userService"; import IAuthService from "../services/interfaces/authService"; import IEmailService from "../services/interfaces/emailService"; import IUserService from "../services/interfaces/userService"; -import { getErrorMessage } from "../utilities/errorUtils"; import { AuthError, AuthErrorCodes } from "../types/authTypes"; +import { getErrorMessage } from "../utilities/errorUtils"; const authRouter: Router = Router(); const userService: IUserService = new UserService(); @@ -209,6 +209,8 @@ authRouter.post( length: 20, numbers: true, }); + const token = getAccessToken(req)!; + const facilitatorId = await authService.getUserIdFromAccessToken(token); const invitedLearnerUser = await userService.createLearner( { firstName: req.body.firstName, @@ -218,7 +220,7 @@ authRouter.post( password: temporaryPassword, status: "Invited", }, - req.body.facilitatorId, + facilitatorId.toString(), ); await authService.sendLearnerInvite( req.body.firstName, diff --git a/backend/rest/userRoutes.ts b/backend/rest/userRoutes.ts index b7bd9948..b3f06fc1 100644 --- a/backend/rest/userRoutes.ts +++ b/backend/rest/userRoutes.ts @@ -11,7 +11,7 @@ import { updateUserDtoValidator, uploadProfilePictureValidator, } from "../middlewares/validators/userValidators"; -import UserModel, { Bookmark } from "../models/user.mgmodel"; +import UserModel, { Bookmark, LearnerModel } from "../models/user.mgmodel"; import nodemailerConfig from "../nodemailer.config"; import AuthService from "../services/implementations/authService"; import CoursePageService from "../services/implementations/coursePageService"; @@ -485,7 +485,7 @@ userRouter.put( ); userRouter.put( - "/learner/markActivityAsCompleted", + "/learner/markActivityCompleted", isAuthorizedByRole(new Set(["Learner"])), completeActivityValidator, async (req, res) => { @@ -514,14 +514,14 @@ userRouter.put( const courseModule = (await courseModuleService.getCourseModule(moduleId))!; - if (!courseModule.pages.includes(activityId)) { + if (!courseModule.pages.some(page => page.id === activityId)) { return res.status(400).send(`The activity provided does not exist on module ${moduleId}.`); } - const updatedUser = await UserModel.findByIdAndUpdate(learnerId, { + const updatedUser = await LearnerModel.findByIdAndUpdate(learnerId, { $addToSet: { [`activitiesCompleted.${unitId}.${moduleId}`]: activityId, - }, + } }, { new: true }); res.status(200).json(updatedUser); diff --git a/backend/services/implementations/__tests__/userService.test.ts b/backend/services/implementations/__tests__/userService.test.ts index 397bbbf8..6c4fb1d1 100644 --- a/backend/services/implementations/__tests__/userService.test.ts +++ b/backend/services/implementations/__tests__/userService.test.ts @@ -1,8 +1,9 @@ -import MgUser from "../../../models/user.mgmodel"; +import MgUser, { LearnerModel } from "../../../models/user.mgmodel"; import UserService from "../userService"; -import { UserDTO } from "../../../types/userTypes"; +import { LearnerDTO, UserDTO } from "../../../types/userTypes"; +import mongoose from "mongoose"; import { testActivities, testCourseModules, testCourseUnits, testLearnersDTO, testLessons, testUsers } from "../../../__mocks__/mockData"; import coursemoduleMgmodel from "../../../models/coursemodule.mgmodel"; import coursepageMgmodel from "../../../models/coursepage.mgmodel"; @@ -73,4 +74,15 @@ describe("mongo userService", (): void => { expect(res).toEqual(1); }); + + it("markActivityCompleted", async () => { + const updatedLearner: LearnerDTO | null = + await LearnerModel.findByIdAndUpdate("67e60671fb8fbc9c9bbb6d8a", { + $addToSet: { + [`activitiesCompleted.${testCourseUnits[0]._id.toString()}.${testCourseModules[0]._id.toString()}`]: testCourseModules[0].pages[0].toString(), + }, + }, { new: true }); + + expect(updatedLearner?.activitiesCompleted.get(testCourseUnits[0]._id.toString())?.get(testCourseModules[0]._id.toString())).toEqual([new mongoose.Types.ObjectId(testCourseModules[0].pages[0])]); + }); }); From 7df97ef49f7f8e85c4f1643d8bbc5f34fd6a33bb Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Mon, 7 Apr 2025 21:20:09 -0400 Subject: [PATCH 05/12] Polish learner progress feature and tests --- backend/__mocks__/mockData.ts | 29 ++++++--- backend/jest.config.ts | 2 +- backend/models/coursepage.mgmodel.ts | 10 ++- backend/rest/userRoutes.ts | 46 ++++++++----- .../__tests__/userService.test.ts | 64 ++++++++++++++----- .../implementations/courseModuleService.ts | 25 ++++---- .../implementations/coursePageService.ts | 9 ++- .../services/implementations/userService.ts | 50 +++++++++++---- .../interfaces/courseModuleService.ts | 2 +- .../services/interfaces/coursePageService.ts | 8 +-- backend/services/interfaces/userService.ts | 30 +++++++-- backend/types/courseTypes.ts | 11 +++- 12 files changed, 201 insertions(+), 85 deletions(-) diff --git a/backend/__mocks__/mockData.ts b/backend/__mocks__/mockData.ts index e3ec197a..89d84949 100644 --- a/backend/__mocks__/mockData.ts +++ b/backend/__mocks__/mockData.ts @@ -31,8 +31,14 @@ export const testCourseModules = [ { _id: "67e60622fb8fbc9c9bbb6d84", displayIndex: 0, - title: "Test Course Module", - pages: testActivities.map(activity => activity._id), + title: "Test Course Module 1", + pages: [testActivities[0]._id], + }, + { + _id: "67e60623fb8fbc9c9bbb6d85", + displayIndex: 1, + title: "Test Course Module 2", + pages: [testActivities[1]._id], }, ]; @@ -41,7 +47,7 @@ export const testCourseUnits = [ _id: "67e6062cfb8fbc9c9bbb6d85", displayIndex: 0, title: "Test Course Unit", - modules: testCourseModules.map(module => module._id), + modules: testCourseModules.map((module) => module._id), }, ]; @@ -82,10 +88,10 @@ export const testLearners = [ facilitator: testFacilitators[0]._id, activitiesCompleted: { [testCourseUnits[0]._id.toString()]: { - [testCourseModules[0]._id.toString()]: [], - // testCourseModules[0].pages.map(page => page.toString()) - } - } + [testCourseModules[0]._id.toString()]: [testActivities[0]._id], + // testCourseModules[0].pages.map(page => page.toString()) + }, + }, }, ]; @@ -99,9 +105,12 @@ export const testLearnersDTO: LearnerDTO[] = [ role: "Learner" as Role, facilitator: testFacilitators[0]._id, activitiesCompleted: new Map([ - [testCourseUnits[0]._id.toString(), new Map([ - [testCourseModules[0]._id.toString(), testCourseModules[0].pages.map(page => page.toString())], - ])] + [ + testCourseUnits[0]._id.toString(), + new Map([ + [testCourseModules[0]._id.toString(), [testActivities[0]._id]], + ]), + ], ]), }, ]; diff --git a/backend/jest.config.ts b/backend/jest.config.ts index 0dbcd906..8460a9b4 100644 --- a/backend/jest.config.ts +++ b/backend/jest.config.ts @@ -84,7 +84,7 @@ export default { // moduleNameMapper: {}, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], + modulePathIgnorePatterns: ["/build/"], // Activates notifications for test results // notify: false, diff --git a/backend/models/coursepage.mgmodel.ts b/backend/models/coursepage.mgmodel.ts index 82862ed1..67f0111d 100644 --- a/backend/models/coursepage.mgmodel.ts +++ b/backend/models/coursepage.mgmodel.ts @@ -26,21 +26,25 @@ import { ElementSkeleton, PageType } from "../types/courseTypes"; // }, // }); -export interface CoursePage extends Document { +interface CoursePageBase extends Document { id: string; title: string; type: PageType; } -export interface LessonPage extends CoursePage { +export interface LessonPage extends CoursePageBase { + type: "Lesson"; source: string; pageIndex: number; } -export interface ActivityPage extends CoursePage { +export interface ActivityPage extends CoursePageBase { + type: "Activity"; layout: [ElementSkeleton]; } +export type CoursePage = LessonPage | ActivityPage; + const baseOptions = { discriminatorKey: "type", }; diff --git a/backend/rest/userRoutes.ts b/backend/rest/userRoutes.ts index b3f06fc1..55c2f115 100644 --- a/backend/rest/userRoutes.ts +++ b/backend/rest/userRoutes.ts @@ -509,26 +509,38 @@ userRouter.put( } if (!courseUnit.modules.includes(moduleId)) { - return res.status(400).send(`The module provided does not exist on unit ${unitId}.`); + return res + .status(400) + .send(`The module provided does not exist on unit ${unitId}.`); } - const courseModule = (await courseModuleService.getCourseModule(moduleId))!; + const courseModule = (await courseModuleService.getCourseModule( + moduleId, + ))!; - if (!courseModule.pages.some(page => page.id === activityId)) { - return res.status(400).send(`The activity provided does not exist on module ${moduleId}.`); + if ( + !courseModule.pages.some( + (page) => page.id === activityId && page.type === "Activity", + ) + ) { + return res + .status(400) + .send( + `The activity provided does not exist on module ${moduleId} or is not an activity.`, + ); } - const updatedUser = await LearnerModel.findByIdAndUpdate(learnerId, { - $addToSet: { - [`activitiesCompleted.${unitId}.${moduleId}`]: activityId, - } - }, { new: true }); - + const updatedUser = (await userService.addActivityToProgress( + learnerId.toString(), + unitId, + moduleId, + activityId, + ))!; res.status(200).json(updatedUser); } catch (error) { res.status(500).send(getErrorMessage(error)); } - } + }, ); userRouter.get( @@ -544,19 +556,21 @@ userRouter.get( return res.status(403).send("Forbidden: User is not a learner."); } - const numCompletedModules = await userService.getNumCompletedModules(learner); + const completedModules = await userService.getCompletedModules(learner); const allUnits = await courseUnitService.getCourseUnits(); - const numModules = allUnits.reduce((acc, unit) => acc + unit.modules.length, 0); + const numModules = allUnits.reduce( + (acc, unit) => acc + unit.modules.length, + 0, + ); res.status(200).send({ - numCompletedModules, + completedModules, numModules, - progressPercentage: (numCompletedModules / numModules) * 100, }); } catch (error) { res.status(500).send(getErrorMessage(error)); } - } + }, ); export default userRouter; diff --git a/backend/services/implementations/__tests__/userService.test.ts b/backend/services/implementations/__tests__/userService.test.ts index 6c4fb1d1..40c691ca 100644 --- a/backend/services/implementations/__tests__/userService.test.ts +++ b/backend/services/implementations/__tests__/userService.test.ts @@ -1,20 +1,34 @@ -import MgUser, { LearnerModel } from "../../../models/user.mgmodel"; +import MgUser, { Learner, LearnerModel } from "../../../models/user.mgmodel"; import UserService from "../userService"; import { LearnerDTO, UserDTO } from "../../../types/userTypes"; import mongoose from "mongoose"; -import { testActivities, testCourseModules, testCourseUnits, testLearnersDTO, testLessons, testUsers } from "../../../__mocks__/mockData"; +import { + testActivities, + testCourseModules, + testCourseUnits, + testLearners, + testLessons, + testUsers, +} from "../../../__mocks__/mockData"; import coursemoduleMgmodel from "../../../models/coursemodule.mgmodel"; import coursepageMgmodel from "../../../models/coursepage.mgmodel"; import courseunitMgmodel from "../../../models/courseunit.mgmodel"; import db from "../../../testUtils/testDb"; +const getLearnerById = async (id: string): Promise => { + const testUser: Learner | null = await LearnerModel.findById(id); + expect(testUser).not.toBeNull(); + const testUserDTO: LearnerDTO = testUser!.toObject(); + return testUserDTO; +}; + jest.mock("firebase-admin", () => { const auth = jest.fn().mockReturnValue({ getUser: jest.fn().mockReturnValue({ email: "test@test.com" }), }); - + const storage = jest.fn().mockReturnValue({ bucket: jest.fn().mockReturnValue({ file: jest.fn().mockReturnValue({ @@ -25,13 +39,13 @@ jest.mock("firebase-admin", () => { upload: jest.fn(), }), }); - + return { auth, storage }; }); jest.mock("firebase-admin/storage", () => { - return { - getDownloadURL: jest.fn().mockReturnValue("https://test.com/image.jpg") + return { + getDownloadURL: jest.fn().mockReturnValue("https://test.com/image.jpg"), }; }); @@ -69,20 +83,36 @@ describe("mongo userService", (): void => { }); it("getNumCompletedModules", async () => { - const testUser = testLearnersDTO[0]; - const res = await userService.getNumCompletedModules(testUser); + let testUserDTO = await getLearnerById(testLearners[0]._id); + const res1 = await userService.getCompletedModules(testUserDTO); + + expect(res1).toEqual(new Set([testCourseModules[0]._id])); + + await userService.addActivityToProgress( + testLearners[0]._id, + testCourseUnits[0]._id, + testCourseModules[1]._id, + testCourseModules[1].pages[0], + ); - expect(res).toEqual(1); + testUserDTO = await getLearnerById(testLearners[0]._id); + const res2 = await userService.getCompletedModules(testUserDTO); + expect(res2).toEqual( + new Set([testCourseModules[0]._id, testCourseModules[1]._id]), + ); }); it("markActivityCompleted", async () => { - const updatedLearner: LearnerDTO | null = - await LearnerModel.findByIdAndUpdate("67e60671fb8fbc9c9bbb6d8a", { - $addToSet: { - [`activitiesCompleted.${testCourseUnits[0]._id.toString()}.${testCourseModules[0]._id.toString()}`]: testCourseModules[0].pages[0].toString(), - }, - }, { new: true }); - - expect(updatedLearner?.activitiesCompleted.get(testCourseUnits[0]._id.toString())?.get(testCourseModules[0]._id.toString())).toEqual([new mongoose.Types.ObjectId(testCourseModules[0].pages[0])]); + const updatedLearner = await userService.addActivityToProgress( + testLearners[0]._id, + testCourseUnits[0]._id, + testCourseModules[0]._id, + testCourseModules[0].pages[0], + ); + expect( + updatedLearner?.activitiesCompleted + .get(testCourseUnits[0]._id.toString()) + ?.get(testCourseModules[0]._id.toString()), + ).toEqual([new mongoose.Types.ObjectId(testCourseModules[0].pages[0])]); }); }); diff --git a/backend/services/implementations/courseModuleService.ts b/backend/services/implementations/courseModuleService.ts index 39861083..89581092 100644 --- a/backend/services/implementations/courseModuleService.ts +++ b/backend/services/implementations/courseModuleService.ts @@ -73,9 +73,17 @@ class CourseModuleService implements ICourseModuleService { } } - async getCourseModule( - courseModuleId: string, - ): Promise { + private static async fetchPage( + page: Schema.Types.ObjectId, + ): Promise { + const pageObject = await CoursePageModel.findById(page).lean().exec(); + if (!pageObject) { + throw new Error(`Page with id ${page} not found.`); + } + return pageObject; + } + + async getCourseModule(courseModuleId: string): Promise { try { const courseModule: CourseModule | null = await MgCourseModule.findById( courseModuleId, @@ -102,14 +110,9 @@ class CourseModuleService implements ICourseModuleService { const lessonPdfUrl: string | undefined = await fileStorageService.getFile( `course/pdfs/module-${courseModuleId}.pdf`, ); - const fetchPage = async (page: Schema.Types.ObjectId): Promise => { - const pageObject = await CoursePageModel.findById(page).lean().exec(); - if (!pageObject) { - throw new Error(`Page with id ${page} not found.`); - } - return pageObject; - }; - const pageObjects = Promise.all(courseModule.pages.map(fetchPage)); + const pageObjects = Promise.all( + courseModule.pages.map(CourseModuleService.fetchPage), + ); return { ...courseModule, unitId: courseUnit._id.toString(), // eslint-disable-line no-underscore-dangle diff --git a/backend/services/implementations/coursePageService.ts b/backend/services/implementations/coursePageService.ts index c3ba35e0..af60262c 100644 --- a/backend/services/implementations/coursePageService.ts +++ b/backend/services/implementations/coursePageService.ts @@ -6,6 +6,7 @@ import MgCourseModule, { import MgCoursePage from "../../models/coursepage.mgmodel"; import { CoursePageDTO, + CoursePageDTOBase, CreateCoursePageDTO, UpdateCoursePageDTO, } from "../../types/courseTypes"; @@ -16,7 +17,9 @@ import ICoursePageService from "../interfaces/coursePageService"; const Logger = logger(__filename); class CoursePageService implements ICoursePageService { - async getCoursePages(courseModuleId: string): Promise> { + async getCoursePages( + courseModuleId: string, + ): Promise> { try { const courseModule: CourseModule | null = await MgCourseModule.findById( courseModuleId, @@ -72,7 +75,7 @@ class CoursePageService implements ICoursePageService { async createCoursePage( courseModuleId: string, coursePageDTO: CreateCoursePageDTO, - ): Promise { + ): Promise { const session = await startSession(); session.startTransaction(); try { @@ -103,7 +106,7 @@ class CoursePageService implements ICoursePageService { async updateCoursePage( coursePageId: string, coursePageDTO: UpdateCoursePageDTO, - ): Promise { + ): Promise { try { const updatedCoursePage = await MgCoursePage.findByIdAndUpdate( coursePageId, diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index 36c3a4df..a3dc1a95 100644 --- a/backend/services/implementations/userService.ts +++ b/backend/services/implementations/userService.ts @@ -363,15 +363,39 @@ class UserService implements IUserService { } } - async getNumCompletedModules(learner: LearnerDTO): Promise { - let numCompletedModules = 0; - + async addActivityToProgress( + learnerId: string, + unitId: string, + moduleId: string, + activityId: string, + ): Promise { + const updatedUser = await LearnerModel.findByIdAndUpdate( + learnerId, + { + $addToSet: { + [`activitiesCompleted.${unitId}.${moduleId}`]: activityId, + }, + }, + { new: true }, + ); + return updatedUser; + } + + async getCompletedModules(learner: LearnerDTO): Promise> { + let completedModules = new Set(); + for (const unitsMap of learner.activitiesCompleted.values()) { - for (const [moduleId, activitiesCompletedInModule] of unitsMap.entries()) { + for (const [ + moduleId, + activitiesCompletedInModule, + ] of unitsMap.entries()) { try { const module = await courseModuleService.getCourseModule(moduleId); - if (module && activitiesCompletedInModule.length === module.pages.length) { - numCompletedModules += 1; + const moduleActivities = module.pages.filter( + (page) => page.type === "Activity", + ); + if (activitiesCompletedInModule.length === moduleActivities.length) { + completedModules.add(moduleId); } } catch (error) { console.error(`Error finding module ${moduleId}:`, error); @@ -379,16 +403,20 @@ class UserService implements IUserService { } } - return numCompletedModules; + return completedModules; } - async deleteActivityFromProgress(unitId: string, moduleId: string, activityId: string): Promise { + async deleteActivityFromProgress( + unitId: string, + moduleId: string, + activityId: string, + ): Promise { const { modifiedCount } = await LearnerModel.updateMany( {}, { - $pull: { [`activitiesCompleted.${unitId}.${moduleId}`]: activityId } - } - ) + $pull: { [`activitiesCompleted.${unitId}.${moduleId}`]: activityId }, + }, + ); return modifiedCount; } } diff --git a/backend/services/interfaces/courseModuleService.ts b/backend/services/interfaces/courseModuleService.ts index 8833c583..93009f9e 100644 --- a/backend/services/interfaces/courseModuleService.ts +++ b/backend/services/interfaces/courseModuleService.ts @@ -18,7 +18,7 @@ interface ICourseModuleService { * @param courseModuleId the id of the course module we want to fetch * @throws Error if course module was not successfully fetched */ - getCourseModule(courseModuleId: string): Promise; + getCourseModule(courseModuleId: string): Promise; /** * Creates a course module diff --git a/backend/services/interfaces/coursePageService.ts b/backend/services/interfaces/coursePageService.ts index f72e8f3e..1fab7eb7 100644 --- a/backend/services/interfaces/coursePageService.ts +++ b/backend/services/interfaces/coursePageService.ts @@ -1,5 +1,5 @@ import { - CoursePageDTO, + CoursePageDTOBase, CreateCoursePageDTO, UpdateCoursePageDTO, } from "../../types/courseTypes"; @@ -10,7 +10,7 @@ interface ICoursePageService { * @param courseModuleId the id of the module we want to fetch the pages of * @throws Error if course pages were not successfully fetched */ - getCoursePages(courseModuleId: string): Promise>; + getCoursePages(courseModuleId: string): Promise>; /** * Returns 1 course page @@ -28,7 +28,7 @@ interface ICoursePageService { createCoursePage( courseModuleId: string, coursePageDTO: CreateCoursePageDTO, - ): Promise; + ): Promise; /** * Updates 1 specific course page @@ -39,7 +39,7 @@ interface ICoursePageService { updateCoursePage( coursePageId: string, coursePageDTO: UpdateCoursePageDTO, - ): Promise; + ): Promise; /** * Deletes 1 course page diff --git a/backend/services/interfaces/userService.ts b/backend/services/interfaces/userService.ts index 52fe5887..52471a53 100644 --- a/backend/services/interfaces/userService.ts +++ b/backend/services/interfaces/userService.ts @@ -1,4 +1,5 @@ import { ObjectId } from "mongoose"; +import { Learner } from "../../models/user.mgmodel"; import { CreateUserDTO, LearnerDTO, @@ -121,11 +122,26 @@ interface IUserService { changeUserStatus(accessToken: string, newStatus: Status): Promise; /** - * Get the number of completed modules for a learner - * @param learner the learner to get the number of completed modules for - * @returns the number of completed modules + * Add an activity to a learner's progress + * @param learnerId the id of the learner to add the activity to + * @param unitId the id of the unit to add the activity to + * @param moduleId the id of the module to add the activity to + * @param activityId the id of the activity to add to the learner's progress + * @returns the updated learner */ - getNumCompletedModules(learner: LearnerDTO): Promise; + addActivityToProgress( + learnerId: string, + unitId: string, + moduleId: string, + activityId: string, + ): Promise; + + /** + * Get the set of completed modules for a learner + * @param learner the learner to get the set of completed modules for + * @returns the set of completed modules + */ + getCompletedModules(learner: LearnerDTO): Promise>; /** * Delete an activity from every learner's progress @@ -134,6 +150,10 @@ interface IUserService { * @param activityId the activity id of the activity to delete * @returns the number of learners that were updated */ - deleteActivityFromProgress(unitId: string, moduleId: string, activityId: string): Promise; + deleteActivityFromProgress( + unitId: string, + moduleId: string, + activityId: string, + ): Promise; } export default IUserService; diff --git a/backend/types/courseTypes.ts b/backend/types/courseTypes.ts index 569fb7c6..353c601b 100644 --- a/backend/types/courseTypes.ts +++ b/backend/types/courseTypes.ts @@ -29,13 +29,14 @@ export type CreateCourseModuleDTO = Pick; export type UpdateCourseModuleDTO = Pick; export type PageType = "Lesson" | "Activity"; -export type CoursePageDTO = { +export type CoursePageDTOBase = { id: string; title: string; type: PageType; }; -export type LessonPageDTO = CoursePageDTO & { +export type LessonPageDTO = CoursePageDTOBase & { + type: "Lesson"; source: string; }; @@ -47,10 +48,14 @@ export type ElementSkeleton = { h: number; content: string; }; -export type ActivityPageDTO = CoursePageDTO & { + +export type ActivityPageDTO = CoursePageDTOBase & { + type: "Activity"; layout: [ElementSkeleton]; }; +export type CoursePageDTO = LessonPageDTO | ActivityPageDTO; + export type CreateCoursePageDTO = | Pick | Pick From 29c7a450f835833841a3d4c6fafcaeee3df43e62 Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Wed, 9 Apr 2025 22:52:13 -0400 Subject: [PATCH 06/12] Implement setting a user's next page --- backend/models/user.mgmodel.ts | 5 +++ backend/rest/userRoutes.ts | 28 ++++++++++++ .../__tests__/userService.test.ts | 8 ++++ .../services/implementations/userService.ts | 45 +++++++++++++++++++ backend/services/interfaces/userService.ts | 8 ++++ backend/types/userTypes.ts | 1 + 6 files changed, 95 insertions(+) diff --git a/backend/models/user.mgmodel.ts b/backend/models/user.mgmodel.ts index d7eb60f0..d747cdca 100644 --- a/backend/models/user.mgmodel.ts +++ b/backend/models/user.mgmodel.ts @@ -26,6 +26,7 @@ export interface Bookmark { export interface Learner extends User { facilitator: ObjectId; activitiesCompleted: Map>>; + nextPage?: ObjectId; } export interface Facilitator extends User { @@ -134,6 +135,10 @@ const LearnerSchema = new Schema( ], default: {}, }, + nextPage: { + type: mongoose.Schema.Types.ObjectId, + ref: "CoursePage", + }, }, options, ); diff --git a/backend/rest/userRoutes.ts b/backend/rest/userRoutes.ts index 55c2f115..dab05798 100644 --- a/backend/rest/userRoutes.ts +++ b/backend/rest/userRoutes.ts @@ -573,4 +573,32 @@ userRouter.get( }, ); +userRouter.put( + "/learner/viewedPage", + isAuthorizedByRole(new Set(["Learner"])), + async (req, res) => { + const accessToken = getAccessToken(req)!; + try { + const learnerId = await authService.getUserIdFromAccessToken(accessToken); + const learner = await userService.getUserById(learnerId.toString()); + + if (!isLearner(learner)) { + return res.status(403).send("Forbidden: User is not a learner."); + } + + const { unitId, moduleId, pageId } = req.body; + + const updatedUser = await userService.updateNextPage(learnerId.toString(), { + unitId, + moduleId, + pageId, + }); + + res.status(200).json(updatedUser); + } catch (error) { + res.status(500).send(getErrorMessage(error)); + } + }, +); + export default userRouter; diff --git a/backend/services/implementations/__tests__/userService.test.ts b/backend/services/implementations/__tests__/userService.test.ts index 40c691ca..514e1337 100644 --- a/backend/services/implementations/__tests__/userService.test.ts +++ b/backend/services/implementations/__tests__/userService.test.ts @@ -115,4 +115,12 @@ describe("mongo userService", (): void => { ?.get(testCourseModules[0]._id.toString()), ).toEqual([new mongoose.Types.ObjectId(testCourseModules[0].pages[0])]); }); + + it("updateNextPage", async () => { + const updatedLearner = await userService.updateNextPage( + testLearners[0]._id, + { unitId: testCourseUnits[0]._id, moduleId: testCourseModules[0]._id, pageId: testCourseModules[0].pages[0] }, + ); + expect(updatedLearner?.nextPage?.toString()).toEqual(testCourseModules[1].pages[0].toString()); + }); }); diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index a3dc1a95..562d285c 100644 --- a/backend/services/implementations/userService.ts +++ b/backend/services/implementations/userService.ts @@ -19,9 +19,12 @@ import { getErrorCode, getErrorMessage } from "../../utilities/errorUtils"; import logger from "../../utilities/logger"; import IUserService from "../interfaces/userService"; import CourseModuleService from "./courseModuleService"; +import CourseUnitService from "./courseUnitService"; +import courseunitMgmodel from "../../models/courseunit.mgmodel"; const Logger = logger(__filename); const courseModuleService = new CourseModuleService(); +const courseUnitService = new CourseUnitService(); const getMongoUserByAuthId = async (authId: string): Promise => { const user: User | null = await MgUser.findOne({ authId }); @@ -419,6 +422,48 @@ class UserService implements IUserService { ); return modifiedCount; } + + private async getNextPage(unitId: string, moduleId: string, pageId: string): Promise { + const module = await courseModuleService.getCourseModule(moduleId); + const curPageIndex = module.pages.findIndex((page) => page.id === pageId); + const nextPage = module.pages[curPageIndex + 1]; + if (nextPage) { + return nextPage.id; + } + const unit = await courseUnitService.getCourseUnit(unitId); + const curModuleIndex = unit.modules.findIndex((module) => module === moduleId); + const nextModuleId = unit.modules[curModuleIndex + 1]; + if (nextModuleId) { + try { + const nextModule = await courseModuleService.getCourseModule(nextModuleId); + if (nextModule && nextModule.pages.length > 0) { + return nextModule.pages[0].id; + } + } catch (error) {} + } + const nextUnit = await courseunitMgmodel.findOne({displayIndex: unit.displayIndex + 1}); + if (nextUnit) { + const nextModule = await courseModuleService.getCourseModule(nextUnit.modules[0].toString()); + if (nextModule && nextModule.pages.length > 0) { + return nextModule.pages[0].id; + } + } + return null; + } + + async updateNextPage(learnerId: string, justViewed: {unitId: string, moduleId: string, pageId: string}): Promise { + const nextPageId = await this.getNextPage(justViewed.unitId, justViewed.moduleId, justViewed.pageId); + if (!nextPageId) { + Logger.warn(`No next page found for learner ${learnerId} after viewing ${justViewed.pageId} in module ${justViewed.moduleId} of unit ${justViewed.unitId}`); + return null; + } + const updatedUser = await LearnerModel.findByIdAndUpdate( + learnerId, + { nextPage: nextPageId }, + { new: true }, + ); + return updatedUser; + } } export default UserService; diff --git a/backend/services/interfaces/userService.ts b/backend/services/interfaces/userService.ts index 52471a53..a91e6182 100644 --- a/backend/services/interfaces/userService.ts +++ b/backend/services/interfaces/userService.ts @@ -155,5 +155,13 @@ interface IUserService { moduleId: string, activityId: string, ): Promise; + + /** + * Update the next page for a learner + * @param learnerId the id of the learner to update the next page for + * @param justViewed information about the page that the learner just viewed + * @returns the updated learner + */ + updateNextPage(learnerId: string, justViewed: {unitId: string, moduleId: string, pageId: string}): Promise; } export default IUserService; diff --git a/backend/types/userTypes.ts b/backend/types/userTypes.ts index cf9e8585..6a920b2e 100644 --- a/backend/types/userTypes.ts +++ b/backend/types/userTypes.ts @@ -29,6 +29,7 @@ export type UserDTO = { profilePicture?: string; bio?: string; bookmarks: BookmarkDTO[]; + nextPage?: string; }; export type CreateUserDTO = Omit & { From 50aa82baf097eb10db3be985bebe0e7b8f094d4b Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Wed, 9 Apr 2025 23:11:30 -0400 Subject: [PATCH 07/12] Fix linter errors --- backend/__mocks__/firebase-admin/storage.ts | 4 +- backend/__mocks__/mockData.ts | 1 + backend/rest/userRoutes.ts | 26 +++++---- .../__tests__/courseModuleService.test.ts | 7 +-- .../__tests__/userService.test.ts | 13 ++++- .../services/implementations/userService.ts | 56 ++++++++++++++----- backend/services/interfaces/userService.ts | 5 +- 7 files changed, 73 insertions(+), 39 deletions(-) diff --git a/backend/__mocks__/firebase-admin/storage.ts b/backend/__mocks__/firebase-admin/storage.ts index 790b5130..12483d1a 100644 --- a/backend/__mocks__/firebase-admin/storage.ts +++ b/backend/__mocks__/firebase-admin/storage.ts @@ -1,5 +1,5 @@ const getDownloadURL = jest.fn().mockReturnValue("https://test.com/image.jpg"); module.exports = { - getDownloadURL -}; + getDownloadURL, +}; diff --git a/backend/__mocks__/mockData.ts b/backend/__mocks__/mockData.ts index 89d84949..1bf58bbf 100644 --- a/backend/__mocks__/mockData.ts +++ b/backend/__mocks__/mockData.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ import { LearnerDTO, Role, Status } from "../types/userTypes"; export const testLessons = [ diff --git a/backend/rest/userRoutes.ts b/backend/rest/userRoutes.ts index dab05798..8eb87275 100644 --- a/backend/rest/userRoutes.ts +++ b/backend/rest/userRoutes.ts @@ -501,7 +501,6 @@ userRouter.put( const { unitId, moduleId, activityId } = req.body; // Check that everything exists - const courseUnitService = new CourseUnitService(); const courseUnit = await courseUnitService.getCourseUnit(unitId); if (!courseUnit) { @@ -536,9 +535,9 @@ userRouter.put( moduleId, activityId, ))!; - res.status(200).json(updatedUser); + return res.status(200).json(updatedUser); } catch (error) { - res.status(500).send(getErrorMessage(error)); + return res.status(500).send(getErrorMessage(error)); } }, ); @@ -563,12 +562,12 @@ userRouter.get( 0, ); - res.status(200).send({ + return res.status(200).send({ completedModules, numModules, }); } catch (error) { - res.status(500).send(getErrorMessage(error)); + return res.status(500).send(getErrorMessage(error)); } }, ); @@ -588,15 +587,18 @@ userRouter.put( const { unitId, moduleId, pageId } = req.body; - const updatedUser = await userService.updateNextPage(learnerId.toString(), { - unitId, - moduleId, - pageId, - }); + const updatedUser = await userService.updateNextPage( + learnerId.toString(), + { + unitId, + moduleId, + pageId, + }, + ); - res.status(200).json(updatedUser); + return res.status(200).json(updatedUser); } catch (error) { - res.status(500).send(getErrorMessage(error)); + return res.status(500).send(getErrorMessage(error)); } }, ); diff --git a/backend/services/implementations/__tests__/courseModuleService.test.ts b/backend/services/implementations/__tests__/courseModuleService.test.ts index 28c8249c..bab09d03 100644 --- a/backend/services/implementations/__tests__/courseModuleService.test.ts +++ b/backend/services/implementations/__tests__/courseModuleService.test.ts @@ -1,9 +1,6 @@ import db from "../../../testUtils/testDb"; -import CourseModuleService from "../courseModuleService"; describe("courseModuleService", (): void => { - let courseModuleService: CourseModuleService; - beforeAll(async () => { await db.connect(); }); @@ -12,9 +9,7 @@ describe("courseModuleService", (): void => { await db.disconnect(); }); - beforeEach(async () => { - courseModuleService = new CourseModuleService(); - }); + beforeEach(async () => {}); afterEach(async () => { await db.clear(); diff --git a/backend/services/implementations/__tests__/userService.test.ts b/backend/services/implementations/__tests__/userService.test.ts index 514e1337..52e63f84 100644 --- a/backend/services/implementations/__tests__/userService.test.ts +++ b/backend/services/implementations/__tests__/userService.test.ts @@ -1,9 +1,10 @@ +/* eslint-disable no-underscore-dangle */ +import mongoose from "mongoose"; import MgUser, { Learner, LearnerModel } from "../../../models/user.mgmodel"; import UserService from "../userService"; import { LearnerDTO, UserDTO } from "../../../types/userTypes"; -import mongoose from "mongoose"; import { testActivities, testCourseModules, @@ -119,8 +120,14 @@ describe("mongo userService", (): void => { it("updateNextPage", async () => { const updatedLearner = await userService.updateNextPage( testLearners[0]._id, - { unitId: testCourseUnits[0]._id, moduleId: testCourseModules[0]._id, pageId: testCourseModules[0].pages[0] }, + { + unitId: testCourseUnits[0]._id, + moduleId: testCourseModules[0]._id, + pageId: testCourseModules[0].pages[0], + }, + ); + expect(updatedLearner?.nextPage?.toString()).toEqual( + testCourseModules[1].pages[0].toString(), ); - expect(updatedLearner?.nextPage?.toString()).toEqual(testCourseModules[1].pages[0].toString()); }); }); diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index 562d285c..debc2a8b 100644 --- a/backend/services/implementations/userService.ts +++ b/backend/services/implementations/userService.ts @@ -385,14 +385,17 @@ class UserService implements IUserService { } async getCompletedModules(learner: LearnerDTO): Promise> { - let completedModules = new Set(); + const completedModules = new Set(); + // eslint-disable-next-line no-restricted-syntax for (const unitsMap of learner.activitiesCompleted.values()) { + // eslint-disable-next-line no-restricted-syntax for (const [ moduleId, activitiesCompletedInModule, ] of unitsMap.entries()) { try { + // eslint-disable-next-line no-await-in-loop const module = await courseModuleService.getCourseModule(moduleId); const moduleActivities = module.pages.filter( (page) => page.type === "Activity", @@ -400,9 +403,8 @@ class UserService implements IUserService { if (activitiesCompletedInModule.length === moduleActivities.length) { completedModules.add(moduleId); } - } catch (error) { - console.error(`Error finding module ${moduleId}:`, error); - } + // eslint-disable-next-line no-empty + } catch (error) {} } } @@ -423,27 +425,42 @@ class UserService implements IUserService { return modifiedCount; } - private async getNextPage(unitId: string, moduleId: string, pageId: string): Promise { - const module = await courseModuleService.getCourseModule(moduleId); - const curPageIndex = module.pages.findIndex((page) => page.id === pageId); - const nextPage = module.pages[curPageIndex + 1]; + private async getNextPage( + unitId: string, + moduleId: string, + pageId: string, + ): Promise { + const curModule = await courseModuleService.getCourseModule(moduleId); + const curPageIndex = curModule.pages.findIndex( + (page) => page.id === pageId, + ); + const nextPage = curModule.pages[curPageIndex + 1]; if (nextPage) { return nextPage.id; } const unit = await courseUnitService.getCourseUnit(unitId); - const curModuleIndex = unit.modules.findIndex((module) => module === moduleId); + const curModuleIndex = unit.modules.findIndex( + (module) => module === moduleId, + ); const nextModuleId = unit.modules[curModuleIndex + 1]; if (nextModuleId) { try { - const nextModule = await courseModuleService.getCourseModule(nextModuleId); + const nextModule = await courseModuleService.getCourseModule( + nextModuleId, + ); if (nextModule && nextModule.pages.length > 0) { return nextModule.pages[0].id; } + // eslint-disable-next-line no-empty } catch (error) {} } - const nextUnit = await courseunitMgmodel.findOne({displayIndex: unit.displayIndex + 1}); + const nextUnit = await courseunitMgmodel.findOne({ + displayIndex: unit.displayIndex + 1, + }); if (nextUnit) { - const nextModule = await courseModuleService.getCourseModule(nextUnit.modules[0].toString()); + const nextModule = await courseModuleService.getCourseModule( + nextUnit.modules[0].toString(), + ); if (nextModule && nextModule.pages.length > 0) { return nextModule.pages[0].id; } @@ -451,10 +468,19 @@ class UserService implements IUserService { return null; } - async updateNextPage(learnerId: string, justViewed: {unitId: string, moduleId: string, pageId: string}): Promise { - const nextPageId = await this.getNextPage(justViewed.unitId, justViewed.moduleId, justViewed.pageId); + async updateNextPage( + learnerId: string, + justViewed: { unitId: string; moduleId: string; pageId: string }, + ): Promise { + const nextPageId = await this.getNextPage( + justViewed.unitId, + justViewed.moduleId, + justViewed.pageId, + ); if (!nextPageId) { - Logger.warn(`No next page found for learner ${learnerId} after viewing ${justViewed.pageId} in module ${justViewed.moduleId} of unit ${justViewed.unitId}`); + Logger.warn( + `No next page found for learner ${learnerId} after viewing ${justViewed.pageId} in module ${justViewed.moduleId} of unit ${justViewed.unitId}`, + ); return null; } const updatedUser = await LearnerModel.findByIdAndUpdate( diff --git a/backend/services/interfaces/userService.ts b/backend/services/interfaces/userService.ts index a91e6182..a1a7ff4c 100644 --- a/backend/services/interfaces/userService.ts +++ b/backend/services/interfaces/userService.ts @@ -162,6 +162,9 @@ interface IUserService { * @param justViewed information about the page that the learner just viewed * @returns the updated learner */ - updateNextPage(learnerId: string, justViewed: {unitId: string, moduleId: string, pageId: string}): Promise; + updateNextPage( + learnerId: string, + justViewed: { unitId: string; moduleId: string; pageId: string }, + ): Promise; } export default IUserService; From d320b46f047a7e31e17a18ea7b3dced72a412f4b Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Wed, 9 Apr 2025 23:26:29 -0400 Subject: [PATCH 08/12] Polish --- backend/__mocks__/mockData.ts | 4 ++++ backend/rest/userRoutes.ts | 2 +- backend/services/interfaces/coursePageService.ts | 5 ++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/__mocks__/mockData.ts b/backend/__mocks__/mockData.ts index 1bf58bbf..f696a304 100644 --- a/backend/__mocks__/mockData.ts +++ b/backend/__mocks__/mockData.ts @@ -61,6 +61,7 @@ export const testAdmins = [ email: "peter.pan@example.com", status: "Active" as Status, role: "Administrator" as Role, + bookmarks: [], }, ]; @@ -74,6 +75,7 @@ export const testFacilitators = [ status: "Active" as Status, role: "Facilitator" as Role, learners: ["67e60671fb8fbc9c9bbb6d8a"], + bookmarks: [], }, ]; @@ -93,6 +95,7 @@ export const testLearners = [ // testCourseModules[0].pages.map(page => page.toString()) }, }, + bookmarks: [], }, ]; @@ -113,6 +116,7 @@ export const testLearnersDTO: LearnerDTO[] = [ ]), ], ]), + bookmarks: [], }, ]; diff --git a/backend/rest/userRoutes.ts b/backend/rest/userRoutes.ts index 8eb87275..739976c2 100644 --- a/backend/rest/userRoutes.ts +++ b/backend/rest/userRoutes.ts @@ -11,7 +11,7 @@ import { updateUserDtoValidator, uploadProfilePictureValidator, } from "../middlewares/validators/userValidators"; -import UserModel, { Bookmark, LearnerModel } from "../models/user.mgmodel"; +import UserModel, { Bookmark } from "../models/user.mgmodel"; import nodemailerConfig from "../nodemailer.config"; import AuthService from "../services/implementations/authService"; import CoursePageService from "../services/implementations/coursePageService"; diff --git a/backend/services/interfaces/coursePageService.ts b/backend/services/interfaces/coursePageService.ts index 1fab7eb7..833f8660 100644 --- a/backend/services/interfaces/coursePageService.ts +++ b/backend/services/interfaces/coursePageService.ts @@ -17,7 +17,10 @@ interface ICoursePageService { * @param coursePageId the id of the page we want to fetch * @throwsError if course page was not successfully fetched or not found */ - getCoursePage(coursePageId: string, lean?: boolean): Promise; + getCoursePage( + coursePageId: string, + lean?: boolean, + ): Promise; /** * Creates a course page, appended as the last page in the module (for now) From e55a59b1bf6daa03db8718b0a4674b87ed053e82 Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Wed, 9 Apr 2025 23:41:37 -0400 Subject: [PATCH 09/12] Final polishes and more unit tests --- .github/workflows/lint.yml | 13 +++++--- backend/__mocks__/mockData.ts | 31 +++++++++++++++++-- backend/models/courseunit.mgmodel.ts | 3 ++ .../__tests__/userService.test.ts | 30 +++++++++++++++++- .../implementations/courseUnitService.ts | 4 ++- 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9a64ad37..03774583 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Lint codebase +name: Lint and test codebase on: push: @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - + - name: Filter changed files uses: dorny/paths-filter@v2 id: changes @@ -34,7 +34,7 @@ jobs: - "backend/**" python-backend: - "backend/python/**" - + - name: Set up Node.js if: steps.changes.outputs.frontend == 'true' || steps.changes.outputs.typescript-backend == 'true' uses: actions/setup-node@v4 @@ -58,7 +58,12 @@ jobs: if: steps.changes.outputs.typescript-backend == 'true' working-directory: ./backend run: yarn lint - + + - name: Run backend unit tests + if: steps.changes.outputs.typescript-backend == 'true' + working-directory: ./backend + run: yarn test + - name: Lint Python backend if: steps.changes.outputs.python-backend == 'true' working-directory: ./backend/python diff --git a/backend/__mocks__/mockData.ts b/backend/__mocks__/mockData.ts index f696a304..433f2b03 100644 --- a/backend/__mocks__/mockData.ts +++ b/backend/__mocks__/mockData.ts @@ -26,6 +26,20 @@ export const testActivities = [ layout: [], pageIndex: 1, }, + { + _id: "67e60618fb8fbc9c9bbb6d84", + title: "Test Activity 3", + type: "Activity", + layout: [], + pageIndex: 2, + }, + { + _id: "67e60619fb8fbc9c9bbb6d85", + title: "Test Activity 4", + type: "Activity", + layout: [], + pageIndex: 3, + }, ]; export const testCourseModules = [ @@ -41,14 +55,26 @@ export const testCourseModules = [ title: "Test Course Module 2", pages: [testActivities[1]._id], }, + { + _id: "67e60624fb8fbc9c9bbb6d86", + displayIndex: 2, + title: "Test Course Module 3", + pages: [testActivities[2]._id, testActivities[3]._id], + }, ]; export const testCourseUnits = [ { _id: "67e6062cfb8fbc9c9bbb6d85", displayIndex: 0, - title: "Test Course Unit", - modules: testCourseModules.map((module) => module._id), + title: "Test Course Unit 1", + modules: [testCourseModules[0]._id, testCourseModules[1]._id], + }, + { + _id: "67e6062dfb8fbc9c9bbb6d86", + displayIndex: 1, + title: "Test Course Unit 2", + modules: [testCourseModules[2]._id], }, ]; @@ -92,7 +118,6 @@ export const testLearners = [ activitiesCompleted: { [testCourseUnits[0]._id.toString()]: { [testCourseModules[0]._id.toString()]: [testActivities[0]._id], - // testCourseModules[0].pages.map(page => page.toString()) }, }, bookmarks: [], diff --git a/backend/models/courseunit.mgmodel.ts b/backend/models/courseunit.mgmodel.ts index 8e44f084..507e79aa 100644 --- a/backend/models/courseunit.mgmodel.ts +++ b/backend/models/courseunit.mgmodel.ts @@ -1,4 +1,5 @@ import mongoose, { Schema, Document, ObjectId } from "mongoose"; +import mongooseLeanId from "mongoose-lean-id"; export interface CourseUnit extends Document { id: string; @@ -35,4 +36,6 @@ CourseUnitSchema.set("toObject", { }, }); +CourseUnitSchema.plugin(mongooseLeanId); + export default mongoose.model("CourseUnit", CourseUnitSchema); diff --git a/backend/services/implementations/__tests__/userService.test.ts b/backend/services/implementations/__tests__/userService.test.ts index 52e63f84..8f01f57c 100644 --- a/backend/services/implementations/__tests__/userService.test.ts +++ b/backend/services/implementations/__tests__/userService.test.ts @@ -117,7 +117,21 @@ describe("mongo userService", (): void => { ).toEqual([new mongoose.Types.ObjectId(testCourseModules[0].pages[0])]); }); - it("updateNextPage", async () => { + it("updateNextPage (next page is in the same module)", async () => { + const updatedLearner = await userService.updateNextPage( + testLearners[0]._id, + { + unitId: testCourseUnits[1]._id, + moduleId: testCourseModules[2]._id, + pageId: testCourseModules[2].pages[0], + }, + ); + expect(updatedLearner?.nextPage?.toString()).toEqual( + testCourseModules[2].pages[1].toString(), + ); + }); + + it("updateNextPage (next page is in the next module)", async () => { const updatedLearner = await userService.updateNextPage( testLearners[0]._id, { @@ -130,4 +144,18 @@ describe("mongo userService", (): void => { testCourseModules[1].pages[0].toString(), ); }); + + it("updateNextPage (next page is in the next unit)", async () => { + const updatedLearner = await userService.updateNextPage( + testLearners[0]._id, + { + unitId: testCourseUnits[0]._id, + moduleId: testCourseModules[1]._id, + pageId: testCourseModules[1].pages[0], + }, + ); + expect(updatedLearner?.nextPage?.toString()).toEqual( + testCourseModules[2].pages[0].toString(), + ); + }); }); diff --git a/backend/services/implementations/courseUnitService.ts b/backend/services/implementations/courseUnitService.ts index 6817f2f7..ff17f455 100644 --- a/backend/services/implementations/courseUnitService.ts +++ b/backend/services/implementations/courseUnitService.ts @@ -35,7 +35,9 @@ class CourseUnitService implements ICourseUnitService { async getCourseUnit( unitId: string, ): Promise { - const courseUnit: CourseUnit | null = await MgCourseUnit.findById(unitId); + const courseUnit: CourseUnit | null = await MgCourseUnit.findById(unitId) + .lean() + .exec(); if (!courseUnit) { throw new Error(`Course unit with id ${unitId} not found.`); From bbd5ad1bdf76d2c6cb50c999311c7f51fcd05e17 Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Wed, 9 Apr 2025 23:46:12 -0400 Subject: [PATCH 10/12] Lower actions runner OS version for testing to work --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 03774583..3c88cf82 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,7 +18,7 @@ on: jobs: run-lint: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout code uses: actions/checkout@v2 From 3024ba0eaa9ed366eaa503a26191f541f5f72536 Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Wed, 23 Jul 2025 22:07:35 -0400 Subject: [PATCH 11/12] Fix Activity model interface --- backend/models/activity.mgmodel.ts | 4 ++-- backend/models/coursepage.mgmodel.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/models/activity.mgmodel.ts b/backend/models/activity.mgmodel.ts index 8b6c058e..e565226d 100644 --- a/backend/models/activity.mgmodel.ts +++ b/backend/models/activity.mgmodel.ts @@ -1,9 +1,9 @@ import { Document, Schema } from "mongoose"; import { QuestionType } from "../types/activityTypes"; -import CoursePageModel, { CoursePage } from "./coursepage.mgmodel"; +import CoursePageModel, { CoursePageBase } from "./coursepage.mgmodel"; // Base Activity Interface -export interface Activity extends CoursePage { +export interface Activity extends CoursePageBase { questionType: QuestionType; activityNumber: string; questionText: string; diff --git a/backend/models/coursepage.mgmodel.ts b/backend/models/coursepage.mgmodel.ts index 67f0111d..f36a708c 100644 --- a/backend/models/coursepage.mgmodel.ts +++ b/backend/models/coursepage.mgmodel.ts @@ -26,7 +26,7 @@ import { ElementSkeleton, PageType } from "../types/courseTypes"; // }, // }); -interface CoursePageBase extends Document { +export interface CoursePageBase extends Document { id: string; title: string; type: PageType; From a88a5eb7f6d6ac65dd38d5236cef9749570b515f Mon Sep 17 00:00:00 2001 From: Jimmy Liu Date: Mon, 28 Jul 2025 20:02:01 -0400 Subject: [PATCH 12/12] Fix ActivityDTO extension --- backend/models/activity.mgmodel.ts | 2 +- .../implementations/coursePageService.ts | 3 +- backend/types/activityTypes.ts | 24 ++------------- backend/types/courseTypes.ts | 30 +++++++++++++++---- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/backend/models/activity.mgmodel.ts b/backend/models/activity.mgmodel.ts index e565226d..20071307 100644 --- a/backend/models/activity.mgmodel.ts +++ b/backend/models/activity.mgmodel.ts @@ -1,5 +1,5 @@ import { Document, Schema } from "mongoose"; -import { QuestionType } from "../types/activityTypes"; +import { QuestionType } from "../types/courseTypes"; import CoursePageModel, { CoursePageBase } from "./coursepage.mgmodel"; // Base Activity Interface diff --git a/backend/services/implementations/coursePageService.ts b/backend/services/implementations/coursePageService.ts index af60262c..97db76df 100644 --- a/backend/services/implementations/coursePageService.ts +++ b/backend/services/implementations/coursePageService.ts @@ -5,7 +5,6 @@ import MgCourseModule, { } from "../../models/coursemodule.mgmodel"; import MgCoursePage from "../../models/coursepage.mgmodel"; import { - CoursePageDTO, CoursePageDTOBase, CreateCoursePageDTO, UpdateCoursePageDTO, @@ -51,7 +50,7 @@ class CoursePageService implements ICoursePageService { async getCoursePage( coursePageId: string, lean = false, - ): Promise { + ): Promise { try { const coursePage = lean ? await MgCoursePage.findById(coursePageId).lean().exec() diff --git a/backend/types/activityTypes.ts b/backend/types/activityTypes.ts index be580ead..a39215f0 100644 --- a/backend/types/activityTypes.ts +++ b/backend/types/activityTypes.ts @@ -1,30 +1,12 @@ -import { CoursePageDTO } from "./courseTypes"; +import { ActivityPageDTO, QuestionType } from "./courseTypes"; -export enum QuestionType { - MultipleChoice = "MultipleChoice", - MultiSelect = "MultiSelect", - Matching = "Matching", - Table = "Table", - Custom = "Custom", -} - -export interface ActivityDTO extends CoursePageDTO { - questionType: QuestionType; - activityNumber: string; - questionText: string; - instruction: string; - imageUrl?: string; - additionalContext?: string; - userFeedback?: string; -} - -export interface MultipleChoiceActivityDTO extends ActivityDTO { +export interface MultipleChoiceActivityDTO extends ActivityPageDTO { questionType: QuestionType.MultipleChoice; options: string[]; correctAnswer: number; } -export interface MultiSelectActivityDTO extends ActivityDTO { +export interface MultiSelectActivityDTO extends ActivityPageDTO { questionType: QuestionType.MultiSelect; options: string[]; correctAnswers: number[]; diff --git a/backend/types/courseTypes.ts b/backend/types/courseTypes.ts index 353c601b..7139dcee 100644 --- a/backend/types/courseTypes.ts +++ b/backend/types/courseTypes.ts @@ -40,6 +40,24 @@ export type LessonPageDTO = CoursePageDTOBase & { source: string; }; +export enum QuestionType { + MultipleChoice = "MultipleChoice", + MultiSelect = "MultiSelect", + Matching = "Matching", + Table = "Table", + Custom = "Custom", +} + +export interface ActivityPageDTO extends CoursePageDTOBase { + questionType: QuestionType; + activityNumber: string; + questionText: string; + instruction: string; + imageUrl?: string; + additionalContext?: string; + userFeedback?: string; +} + export type ElementSkeleton = { id: string; x: number; @@ -49,21 +67,21 @@ export type ElementSkeleton = { content: string; }; -export type ActivityPageDTO = CoursePageDTOBase & { - type: "Activity"; - layout: [ElementSkeleton]; -}; +// export type ActivityPageDTO = CoursePageDTOBase & { +// type: "Activity"; +// layout: [ElementSkeleton]; +// }; export type CoursePageDTO = LessonPageDTO | ActivityPageDTO; export type CreateCoursePageDTO = | Pick | Pick - | Pick; + | Pick; export type UpdateCoursePageDTO = | Pick | Pick - | Pick; + | Pick; export enum InteractiveElementType { TextInput = "TextInput",