Merge remote-tracking branch 'origin/dev' into feat/pagina-om-leerpaden-te-bekijken-#41
# Conflicts: # backend/src/controllers/learning-objects.ts # frontend/src/controllers/base-controller.ts
This commit is contained in:
		
						commit
						99dc346dc1
					
				
					 155 changed files with 3463 additions and 2931 deletions
				
			
		|  | @ -8,14 +8,4 @@ export default [ | ||||||
|             globals: globals.node, |             globals: globals.node, | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
| 
 |  | ||||||
|     { |  | ||||||
|         files: ['tests/**/*.ts'], |  | ||||||
|         languageOptions: { |  | ||||||
|             globals: globals.node, |  | ||||||
|         }, |  | ||||||
|         rules: { |  | ||||||
|             'no-console': 'off', |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
|         "format": "prettier --write src/", |         "format": "prettier --write src/", | ||||||
|         "format-check": "prettier --check src/", |         "format-check": "prettier --check src/", | ||||||
|         "lint": "eslint . --fix", |         "lint": "eslint . --fix", | ||||||
|         "test:unit": "vitest" |         "test:unit": "vitest --run" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@mikro-orm/core": "6.4.9", |         "@mikro-orm/core": "6.4.9", | ||||||
|  |  | ||||||
|  | @ -5,15 +5,16 @@ import cors from './middleware/cors.js'; | ||||||
| import { getLogger, Logger } from './logging/initalize.js'; | import { getLogger, Logger } from './logging/initalize.js'; | ||||||
| import { responseTimeLogger } from './logging/responseTimeLogger.js'; | import { responseTimeLogger } from './logging/responseTimeLogger.js'; | ||||||
| import responseTime from 'response-time'; | import responseTime from 'response-time'; | ||||||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | import { envVars, getNumericEnvVar } from './util/envVars.js'; | ||||||
| import apiRouter from './routes/router.js'; | import apiRouter from './routes/router.js'; | ||||||
| import swaggerMiddleware from './swagger.js'; | import swaggerMiddleware from './swagger.js'; | ||||||
| import swaggerUi from 'swagger-ui-express'; | import swaggerUi from 'swagger-ui-express'; | ||||||
|  | import { errorHandler } from './middleware/error-handling/error-handler.js'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
| const app: Express = express(); | const app: Express = express(); | ||||||
| const port: string | number = getNumericEnvVar(EnvVars.Port); | const port: string | number = getNumericEnvVar(envVars.Port); | ||||||
| 
 | 
 | ||||||
| app.use(express.json()); | app.use(express.json()); | ||||||
| app.use(cors); | app.use(cors); | ||||||
|  | @ -26,7 +27,9 @@ app.use('/api', apiRouter); | ||||||
| // Swagger
 | // Swagger
 | ||||||
| app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); | app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); | ||||||
| 
 | 
 | ||||||
| async function startServer() { | app.use(errorHandler); | ||||||
|  | 
 | ||||||
|  | async function startServer(): Promise<void> { | ||||||
|     await initORM(); |     await initORM(); | ||||||
| 
 | 
 | ||||||
|     app.listen(port, () => { |     app.listen(port, () => { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { EnvVars, getEnvVar } from './util/envvars.js'; | import { envVars, getEnvVar } from './util/envVars.js'; | ||||||
| 
 | 
 | ||||||
| // API
 | // API
 | ||||||
| export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); | export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl); | ||||||
| export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); | export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage); | ||||||
| 
 | 
 | ||||||
| export const FALLBACK_SEQ_NUM = 1; | export const FALLBACK_SEQ_NUM = 1; | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import { Request, Response } from 'express'; | ||||||
| import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; | import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; | ||||||
| import { AssignmentDTO } from '../interfaces/assignment.js'; | import { AssignmentDTO } from '../interfaces/assignment.js'; | ||||||
| 
 | 
 | ||||||
| // Typescript is annoy with with parameter forwarding from class.ts
 | // Typescript is annoying with parameter forwarding from class.ts
 | ||||||
| interface AssignmentParams { | interface AssignmentParams { | ||||||
|     classid: string; |     classid: string; | ||||||
|     id: string; |     id: string; | ||||||
|  | @ -41,7 +41,7 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | ||||||
|     const id = +req.params.id; |     const id = Number(req.params.id); | ||||||
|     const classid = req.params.classid; |     const classid = req.params.classid; | ||||||
| 
 | 
 | ||||||
|     if (isNaN(id)) { |     if (isNaN(id)) { | ||||||
|  | @ -61,7 +61,7 @@ export async function getAssignmentHandler(req: Request<AssignmentParams>, res: | ||||||
| 
 | 
 | ||||||
| export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | ||||||
|     const classid = req.params.classid; |     const classid = req.params.classid; | ||||||
|     const assignmentNumber = +req.params.id; |     const assignmentNumber = Number(req.params.id); | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentNumber)) { |     if (isNaN(assignmentNumber)) { | ||||||
|  |  | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | import { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
| 
 | 
 | ||||||
| type FrontendIdpConfig = { | interface FrontendIdpConfig { | ||||||
|     authority: string; |     authority: string; | ||||||
|     clientId: string; |     clientId: string; | ||||||
|     scope: string; |     scope: string; | ||||||
|     responseType: string; |     responseType: string; | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type FrontendAuthConfig = { | interface FrontendAuthConfig { | ||||||
|     student: FrontendIdpConfig; |     student: FrontendIdpConfig; | ||||||
|     teacher: FrontendIdpConfig; |     teacher: FrontendIdpConfig; | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| const SCOPE = 'openid profile email'; | const SCOPE = 'openid profile email'; | ||||||
| const RESPONSE_TYPE = 'code'; | const RESPONSE_TYPE = 'code'; | ||||||
|  | @ -18,14 +18,14 @@ const RESPONSE_TYPE = 'code'; | ||||||
| export function getFrontendAuthConfig(): FrontendAuthConfig { | export function getFrontendAuthConfig(): FrontendAuthConfig { | ||||||
|     return { |     return { | ||||||
|         student: { |         student: { | ||||||
|             authority: getEnvVar(EnvVars.IdpStudentUrl), |             authority: getEnvVar(envVars.IdpStudentUrl), | ||||||
|             clientId: getEnvVar(EnvVars.IdpStudentClientId), |             clientId: getEnvVar(envVars.IdpStudentClientId), | ||||||
|             scope: SCOPE, |             scope: SCOPE, | ||||||
|             responseType: RESPONSE_TYPE, |             responseType: RESPONSE_TYPE, | ||||||
|         }, |         }, | ||||||
|         teacher: { |         teacher: { | ||||||
|             authority: getEnvVar(EnvVars.IdpTeacherUrl), |             authority: getEnvVar(envVars.IdpTeacherUrl), | ||||||
|             clientId: getEnvVar(EnvVars.IdpTeacherClientId), |             clientId: getEnvVar(envVars.IdpTeacherClientId), | ||||||
|             scope: SCOPE, |             scope: SCOPE, | ||||||
|             responseType: RESPONSE_TYPE, |             responseType: RESPONSE_TYPE, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  | @ -12,14 +12,14 @@ interface GroupParams { | ||||||
| export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> { | export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> { | ||||||
|     const classId = req.params.classid; |     const classId = req.params.classid; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|     const assignmentId = +req.params.assignmentid; |     const assignmentId = Number(req.params.assignmentid); | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentId)) { |     if (isNaN(assignmentId)) { | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const groupId = +req.params.groupid!; // Can't be undefined
 |     const groupId = Number(req.params.groupid!); // Can't be undefined
 | ||||||
| 
 | 
 | ||||||
|     if (isNaN(groupId)) { |     if (isNaN(groupId)) { | ||||||
|         res.status(400).json({ error: 'Group id must be a number' }); |         res.status(400).json({ error: 'Group id must be a number' }); | ||||||
|  | @ -40,7 +40,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< | ||||||
|     const classId = req.params.classid; |     const classId = req.params.classid; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const assignmentId = +req.params.assignmentid; |     const assignmentId = Number(req.params.assignmentid); | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentId)) { |     if (isNaN(assignmentId)) { | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||||
|  | @ -56,7 +56,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< | ||||||
| 
 | 
 | ||||||
| export async function createGroupHandler(req: Request, res: Response): Promise<void> { | export async function createGroupHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classid = req.params.classid; |     const classid = req.params.classid; | ||||||
|     const assignmentId = +req.params.assignmentid; |     const assignmentId = Number(req.params.assignmentid); | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentId)) { |     if (isNaN(assignmentId)) { | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||||
|  | @ -78,14 +78,14 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P | ||||||
|     const classId = req.params.classid; |     const classId = req.params.classid; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const assignmentId = +req.params.assignmentid; |     const assignmentId = Number(req.params.assignmentid); | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentId)) { |     if (isNaN(assignmentId)) { | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const groupId = +req.params.groupid!; // Can't be undefined
 |     const groupId = Number(req.params.groupid); // Can't be undefined
 | ||||||
| 
 | 
 | ||||||
|     if (isNaN(groupId)) { |     if (isNaN(groupId)) { | ||||||
|         res.status(400).json({ error: 'Group id must be a number' }); |         res.status(400).json({ error: 'Group id must be a number' }); | ||||||
|  |  | ||||||
|  | @ -2,18 +2,19 @@ import { Request, Response } from 'express'; | ||||||
| import { FALLBACK_LANG } from '../config.js'; | import { FALLBACK_LANG } from '../config.js'; | ||||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js'; | import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js'; | ||||||
| import learningObjectService from '../services/learning-objects/learning-object-service.js'; | import learningObjectService from '../services/learning-objects/learning-object-service.js'; | ||||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; |  | ||||||
| import { Language } from '../entities/content/language.js'; | import { Language } from '../entities/content/language.js'; | ||||||
| import {BadRequestException, NotFoundException} from '../exceptions.js'; |  | ||||||
| import attachmentService from '../services/learning-objects/attachment-service.js'; | import attachmentService from '../services/learning-objects/attachment-service.js'; | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
|  | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
|  | import { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
| 
 | 
 | ||||||
| function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | ||||||
|     if (!req.params.hruid) { |     if (!req.params.hruid) { | ||||||
|         throw new BadRequestException('HRUID is required.'); |         throw new BadRequestException('HRUID is required.'); | ||||||
|     } |     } | ||||||
|     return { |     return { | ||||||
|         hruid: req.params.hruid as string, |         hruid: req.params.hruid, | ||||||
|         language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, |         language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language, | ||||||
|         version: parseInt(req.query.version as string), |         version: parseInt(req.query.version as string), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | @ -23,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif | ||||||
|         throw new BadRequestException('HRUID is required.'); |         throw new BadRequestException('HRUID is required.'); | ||||||
|     } |     } | ||||||
|     return { |     return { | ||||||
|         hruid: req.params.hruid as string, |         hruid: req.params.hruid, | ||||||
|         language: (req.query.language as Language) || FALLBACK_LANG, |         language: (req.query.language as Language) || FALLBACK_LANG, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,13 +2,14 @@ import { Request, Response } from 'express'; | ||||||
| import { themes } from '../data/themes.js'; | import { themes } from '../data/themes.js'; | ||||||
| import { FALLBACK_LANG } from '../config.js'; | import { FALLBACK_LANG } from '../config.js'; | ||||||
| import learningPathService from '../services/learning-paths/learning-path-service.js'; | import learningPathService from '../services/learning-paths/learning-path-service.js'; | ||||||
| import { BadRequestException, NotFoundException } from '../exceptions.js'; |  | ||||||
| import { Language } from '../entities/content/language.js'; | import { Language } from '../entities/content/language.js'; | ||||||
| import { | import { | ||||||
|     PersonalizationTarget, |     PersonalizationTarget, | ||||||
|     personalizedForGroup, |     personalizedForGroup, | ||||||
|     personalizedForStudent, |     personalizedForStudent, | ||||||
| } from '../services/learning-paths/learning-path-personalization-util.js'; | } from '../services/learning-paths/learning-path-personalization-util.js'; | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
|  | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Fetch learning paths based on query parameters. |  * Fetch learning paths based on query parameters. | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu | ||||||
|     return { |     return { | ||||||
|         hruid, |         hruid, | ||||||
|         language: (lang as Language) || FALLBACK_LANG, |         language: (lang as Language) || FALLBACK_LANG, | ||||||
|         version: +version, |         version: Number(version), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -46,7 +46,7 @@ export async function getStudentHandler(req: Request, res: Response): Promise<vo | ||||||
|     res.json(user); |     res.json(user); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createStudentHandler(req: Request, res: Response) { | export async function createStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const userData = req.body as StudentDTO; |     const userData = req.body as StudentDTO; | ||||||
| 
 | 
 | ||||||
|     if (!userData.username || !userData.firstName || !userData.lastName) { |     if (!userData.username || !userData.firstName || !userData.lastName) { | ||||||
|  | @ -68,7 +68,7 @@ export async function createStudentHandler(req: Request, res: Response) { | ||||||
|     res.status(201).json(newUser); |     res.status(201).json(newUser); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteStudentHandler(req: Request, res: Response) { | export async function deleteStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     if (!username) { | ||||||
|  | @ -93,9 +93,7 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro | ||||||
| 
 | 
 | ||||||
|     const classes = await getStudentClasses(username, full); |     const classes = await getStudentClasses(username, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ classes: classes }); | ||||||
|         classes: classes, |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO
 | // TODO
 | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ interface SubmissionParams { | ||||||
| 
 | 
 | ||||||
| export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> { | export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> { | ||||||
|     const lohruid = req.params.hruid; |     const lohruid = req.params.hruid; | ||||||
|     const submissionNumber = +req.params.id; |     const submissionNumber = Number(req.params.id); | ||||||
| 
 | 
 | ||||||
|     if (isNaN(submissionNumber)) { |     if (isNaN(submissionNumber)) { | ||||||
|         res.status(400).json({ error: 'Submission number is not a number' }); |         res.status(400).json({ error: 'Submission number is not a number' }); | ||||||
|  | @ -30,7 +30,7 @@ export async function getSubmissionHandler(req: Request<SubmissionParams>, res: | ||||||
|     res.json(submission); |     res.json(submission); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createSubmissionHandler(req: Request, res: Response) { | export async function createSubmissionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const submissionDTO = req.body as SubmissionDTO; |     const submissionDTO = req.body as SubmissionDTO; | ||||||
| 
 | 
 | ||||||
|     const submission = await createSubmission(submissionDTO); |     const submission = await createSubmission(submissionDTO); | ||||||
|  | @ -43,9 +43,9 @@ export async function createSubmissionHandler(req: Request, res: Response) { | ||||||
|     res.json(submission); |     res.json(submission); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteSubmissionHandler(req: Request, res: Response) { | export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const hruid = req.params.hruid; |     const hruid = req.params.hruid; | ||||||
|     const submissionNumber = +req.params.id; |     const submissionNumber = Number(req.params.id); | ||||||
| 
 | 
 | ||||||
|     const lang = languageMap[req.query.language as string] || Language.Dutch; |     const lang = languageMap[req.query.language as string] || Language.Dutch; | ||||||
|     const version = (req.query.version || 1) as number; |     const version = (req.query.version || 1) as number; | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ export async function getTeacherHandler(req: Request, res: Response): Promise<vo | ||||||
|     res.json(user); |     res.json(user); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createTeacherHandler(req: Request, res: Response) { | export async function createTeacherHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const userData = req.body as TeacherDTO; |     const userData = req.body as TeacherDTO; | ||||||
| 
 | 
 | ||||||
|     if (!userData.username || !userData.firstName || !userData.lastName) { |     if (!userData.username || !userData.firstName || !userData.lastName) { | ||||||
|  | @ -63,7 +63,7 @@ export async function createTeacherHandler(req: Request, res: Response) { | ||||||
|     res.status(201).json(newUser); |     res.status(201).json(newUser); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteTeacherHandler(req: Request, res: Response) { | export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     if (!username) { | ||||||
|  | @ -83,7 +83,7 @@ export async function deleteTeacherHandler(req: Request, res: Response) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username as string; |     const username = req.params.username; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     if (!username) { | ||||||
|  | @ -102,7 +102,7 @@ export async function getTeacherClassHandler(req: Request, res: Response): Promi | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username as string; |     const username = req.params.username; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     if (!username) { | ||||||
|  | @ -121,7 +121,7 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username as string; |     const username = req.params.username; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     if (!username) { |     if (!username) { | ||||||
|  |  | ||||||
|  | @ -3,25 +3,23 @@ import { themes } from '../data/themes.js'; | ||||||
| import { loadTranslations } from '../util/translation-helper.js'; | import { loadTranslations } from '../util/translation-helper.js'; | ||||||
| 
 | 
 | ||||||
| interface Translations { | interface Translations { | ||||||
|     curricula_page: { |     curricula_page: Record<string, { title: string; description?: string }>; | ||||||
|         [key: string]: { title: string; description?: string }; |  | ||||||
|     }; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getThemesHandler(req: Request, res: Response) { | export function getThemesHandler(req: Request, res: Response): void { | ||||||
|     const language = (req.query.language as string)?.toLowerCase() || 'nl'; |     const language = ((req.query.language as string) || 'nl').toLowerCase(); | ||||||
|     const translations = loadTranslations<Translations>(language); |     const translations = loadTranslations<Translations>(language); | ||||||
|     const themeList = themes.map((theme) => ({ |     const themeList = themes.map((theme) => ({ | ||||||
|         key: theme.title, |         key: theme.title, | ||||||
|         title: translations.curricula_page[theme.title]?.title || theme.title, |         title: translations.curricula_page[theme.title].title || theme.title, | ||||||
|         description: translations.curricula_page[theme.title]?.description, |         description: translations.curricula_page[theme.title].description, | ||||||
|         image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, |         image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, | ||||||
|     })); |     })); | ||||||
| 
 | 
 | ||||||
|     res.json(themeList); |     res.json(themeList); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getHruidsByThemeHandler(req: Request, res: Response) { | export function getHruidsByThemeHandler(req: Request, res: Response): void { | ||||||
|     const themeKey = req.params.theme; |     const themeKey = req.params.theme; | ||||||
| 
 | 
 | ||||||
|     if (!themeKey) { |     if (!themeKey) { | ||||||
|  |  | ||||||
|  | @ -3,13 +3,13 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; | ||||||
| import { Class } from '../../entities/classes/class.entity.js'; | import { Class } from '../../entities/classes/class.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class AssignmentRepository extends DwengoEntityRepository<Assignment> { | export class AssignmentRepository extends DwengoEntityRepository<Assignment> { | ||||||
|     public findByClassAndId(within: Class, id: number): Promise<Assignment | null> { |     public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> { | ||||||
|         return this.findOne({ within: within, id: id }); |         return this.findOne({ within: within, id: id }); | ||||||
|     } |     } | ||||||
|     public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { |     public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { | ||||||
|         return this.findAll({ where: { within: within } }); |         return this.findAll({ where: { within: within } }); | ||||||
|     } |     } | ||||||
|     public deleteByClassAndId(within: Class, id: number): Promise<void> { |     public async deleteByClassAndId(within: Class, id: number): Promise<void> { | ||||||
|         return this.deleteWhere({ within: within, id: id }); |         return this.deleteWhere({ within: within, id: id }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class GroupRepository extends DwengoEntityRepository<Group> { | export class GroupRepository extends DwengoEntityRepository<Group> { | ||||||
|     public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { |     public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { | ||||||
|         return this.findOne( |         return this.findOne( | ||||||
|             { |             { | ||||||
|                 assignment: assignment, |                 assignment: assignment, | ||||||
|  | @ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> { | ||||||
|             { populate: ['members'] } |             { populate: ['members'] } | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|     public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { |     public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { | ||||||
|         return this.findAll({ |         return this.findAll({ | ||||||
|             where: { assignment: assignment }, |             where: { assignment: assignment }, | ||||||
|             populate: ['members'], |             populate: ['members'], | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     public findAllGroupsWithStudent(student: Student): Promise<Group[]> { |     public async findAllGroupsWithStudent(student: Student): Promise<Group[]> { | ||||||
|         return this.find({ members: student }, { populate: ['members'] }); |         return this.find({ members: student }, { populate: ['members'] }); | ||||||
|     } |     } | ||||||
|     public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { |     public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<void> { | ||||||
|         return this.deleteWhere({ |         return this.deleteWhere({ | ||||||
|             assignment: assignment, |             assignment: assignment, | ||||||
|             groupNumber: groupNumber, |             groupNumber: groupNumber, | ||||||
|  |  | ||||||
|  | @ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class SubmissionRepository extends DwengoEntityRepository<Submission> { | export class SubmissionRepository extends DwengoEntityRepository<Submission> { | ||||||
|     public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> { |     public async findSubmissionByLearningObjectAndSubmissionNumber( | ||||||
|  |         loId: LearningObjectIdentifier, | ||||||
|  |         submissionNumber: number | ||||||
|  |     ): Promise<Submission | null> { | ||||||
|         return this.findOne({ |         return this.findOne({ | ||||||
|             learningObjectHruid: loId.hruid, |             learningObjectHruid: loId.hruid, | ||||||
|             learningObjectLanguage: loId.language, |             learningObjectLanguage: loId.language, | ||||||
|  | @ -14,7 +17,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> { |     public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> { | ||||||
|         return this.findOne( |         return this.findOne( | ||||||
|             { |             { | ||||||
|                 learningObjectHruid: loId.hruid, |                 learningObjectHruid: loId.hruid, | ||||||
|  | @ -26,7 +29,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> { | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> { |     public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> { | ||||||
|         return this.findOne( |         return this.findOne( | ||||||
|             { |             { | ||||||
|                 learningObjectHruid: loId.hruid, |                 learningObjectHruid: loId.hruid, | ||||||
|  | @ -38,15 +41,15 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> { | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> { |     public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> { | ||||||
|         return this.find({ onBehalfOf: group }); |         return this.find({ onBehalfOf: group }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { |     public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { | ||||||
|         return this.find({ submitter: student }); |         return this.find({ submitter: student }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> { |     public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> { | ||||||
|         return this.deleteWhere({ |         return this.deleteWhere({ | ||||||
|             learningObjectHruid: loId.hruid, |             learningObjectHruid: loId.hruid, | ||||||
|             learningObjectLanguage: loId.language, |             learningObjectLanguage: loId.language, | ||||||
|  |  | ||||||
|  | @ -4,13 +4,13 @@ import { ClassJoinRequest } from '../../entities/classes/class-join-request.enti | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> { | export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> { | ||||||
|     public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { |     public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { | ||||||
|         return this.findAll({ where: { requester: requester } }); |         return this.findAll({ where: { requester: requester } }); | ||||||
|     } |     } | ||||||
|     public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { |     public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { | ||||||
|         return this.findAll({ where: { class: clazz } }); |         return this.findAll({ where: { class: clazz } }); | ||||||
|     } |     } | ||||||
|     public deleteBy(requester: Student, clazz: Class): Promise<void> { |     public async deleteBy(requester: Student, clazz: Class): Promise<void> { | ||||||
|         return this.deleteWhere({ requester: requester, class: clazz }); |         return this.deleteWhere({ requester: requester, class: clazz }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js'; | ||||||
| import { Teacher } from '../../entities/users/teacher.entity'; | import { Teacher } from '../../entities/users/teacher.entity'; | ||||||
| 
 | 
 | ||||||
| export class ClassRepository extends DwengoEntityRepository<Class> { | export class ClassRepository extends DwengoEntityRepository<Class> { | ||||||
|     public findById(id: string): Promise<Class | null> { |     public async findById(id: string): Promise<Class | null> { | ||||||
|         return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); |         return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); | ||||||
|     } |     } | ||||||
|     public deleteById(id: string): Promise<void> { |     public async deleteById(id: string): Promise<void> { | ||||||
|         return this.deleteWhere({ classId: id }); |         return this.deleteWhere({ classId: id }); | ||||||
|     } |     } | ||||||
|     public findByStudent(student: Student): Promise<Class[]> { |     public async findByStudent(student: Student): Promise<Class[]> { | ||||||
|         return this.find( |         return this.find( | ||||||
|             { students: student }, |             { students: student }, | ||||||
|             { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
 |             { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
 | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findByTeacher(teacher: Teacher): Promise<Class[]> { |     public async findByTeacher(teacher: Teacher): Promise<Class[]> { | ||||||
|         return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); |         return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,16 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent | ||||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> { | export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> { | ||||||
|     public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> { |     public async findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> { | ||||||
|         return this.findAll({ where: { class: clazz } }); |         return this.findAll({ where: { class: clazz } }); | ||||||
|     } |     } | ||||||
|     public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> { |     public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> { | ||||||
|         return this.findAll({ where: { sender: sender } }); |         return this.findAll({ where: { sender: sender } }); | ||||||
|     } |     } | ||||||
|     public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> { |     public async findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> { | ||||||
|         return this.findAll({ where: { receiver: receiver } }); |         return this.findAll({ where: { receiver: receiver } }); | ||||||
|     } |     } | ||||||
|     public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> { |     public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> { | ||||||
|         return this.deleteWhere({ |         return this.deleteWhere({ | ||||||
|             sender: sender, |             sender: sender, | ||||||
|             receiver: receiver, |             receiver: receiver, | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import { Language } from '../../entities/content/language'; | ||||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; | ||||||
| 
 | 
 | ||||||
| export class AttachmentRepository extends DwengoEntityRepository<Attachment> { | export class AttachmentRepository extends DwengoEntityRepository<Attachment> { | ||||||
|     public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> { |     public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> { | ||||||
|         return this.findOne({ |         return this.findOne({ | ||||||
|             learningObject: { |             learningObject: { | ||||||
|                 hruid: learningObjectId.hruid, |                 hruid: learningObjectId.hruid, | ||||||
|  | @ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository<Attachment> { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise<Attachment | null> { |     public async findByMostRecentVersionOfLearningObjectAndName( | ||||||
|  |         hruid: string, | ||||||
|  |         language: Language, | ||||||
|  |         attachmentName: string | ||||||
|  |     ): Promise<Attachment | null> { | ||||||
|         return this.findOne( |         return this.findOne( | ||||||
|             { |             { | ||||||
|                 learningObject: { |                 learningObject: { | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ import { Language } from '../../entities/content/language.js'; | ||||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { | export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { | ||||||
|     public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { |     public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { | ||||||
|         return this.findOne( |         return this.findOne( | ||||||
|             { |             { | ||||||
|                 hruid: identifier.hruid, |                 hruid: identifier.hruid, | ||||||
|  | @ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findLatestByHruidAndLanguage(hruid: string, language: Language) { |     public async findLatestByHruidAndLanguage(hruid: string, language: Language): Promise<LearningObject | null> { | ||||||
|         return this.findOne( |         return this.findOne( | ||||||
|             { |             { | ||||||
|                 hruid: hruid, |                 hruid: hruid, | ||||||
|  | @ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> { |     public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> { | ||||||
|         return this.find( |         return this.find( | ||||||
|             { admins: teacher }, |             { admins: teacher }, | ||||||
|             { populate: ['admins'] } // Make sure to load admin relations
 |             { populate: ['admins'] } // Make sure to load admin relations
 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { LearningPath } from '../../entities/content/learning-path.entity.js'; | ||||||
| import { Language } from '../../entities/content/language.js'; | import { Language } from '../../entities/content/language.js'; | ||||||
| 
 | 
 | ||||||
| export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | ||||||
|     public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { |     public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { | ||||||
|         return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); |         return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,14 @@ | ||||||
| import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | ||||||
|  | import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js'; | ||||||
| 
 | 
 | ||||||
| export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | ||||||
|     public async save(entity: T) { |     public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> { | ||||||
|         const em = this.getEntityManager(); |         if (options?.preventOverwrite && (await this.findOne(entity))) { | ||||||
|         em.persist(entity); |             throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); | ||||||
|         await em.flush(); |         } | ||||||
|  |         await this.getEntityManager().persistAndFlush(entity); | ||||||
|     } |     } | ||||||
|     public async deleteWhere(query: FilterQuery<T>) { |     public async deleteWhere(query: FilterQuery<T>): Promise<void> { | ||||||
|         const toDelete = await this.findOne(query); |         const toDelete = await this.findOne(query); | ||||||
|         const em = this.getEntityManager(); |         const em = this.getEntityManager(); | ||||||
|         if (toDelete) { |         if (toDelete) { | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js'; | ||||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class AnswerRepository extends DwengoEntityRepository<Answer> { | export class AnswerRepository extends DwengoEntityRepository<Answer> { | ||||||
|     public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { |     public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { | ||||||
|         const answerEntity = this.create({ |         const answerEntity = this.create({ | ||||||
|             toQuestion: answer.toQuestion, |             toQuestion: answer.toQuestion, | ||||||
|             author: answer.author, |             author: answer.author, | ||||||
|  | @ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> { | ||||||
|         }); |         }); | ||||||
|         return this.insert(answerEntity); |         return this.insert(answerEntity); | ||||||
|     } |     } | ||||||
|     public findAllAnswersToQuestion(question: Question): Promise<Answer[]> { |     public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> { | ||||||
|         return this.findAll({ |         return this.findAll({ | ||||||
|             where: { toQuestion: question }, |             where: { toQuestion: question }, | ||||||
|             orderBy: { sequenceNumber: 'ASC' }, |             orderBy: { sequenceNumber: 'ASC' }, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { |     public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { | ||||||
|         return this.deleteWhere({ |         return this.deleteWhere({ | ||||||
|             toQuestion: question, |             toQuestion: question, | ||||||
|             sequenceNumber: sequenceNumber, |             sequenceNumber: sequenceNumber, | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ import { Student } from '../../entities/users/student.entity.js'; | ||||||
| import { LearningObject } from '../../entities/content/learning-object.entity.js'; | import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class QuestionRepository extends DwengoEntityRepository<Question> { | export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|     public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { |     public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { | ||||||
|         const questionEntity = this.create({ |         const questionEntity = this.create({ | ||||||
|             learningObjectHruid: question.loId.hruid, |             learningObjectHruid: question.loId.hruid, | ||||||
|             learningObjectLanguage: question.loId.language, |             learningObjectLanguage: question.loId.language, | ||||||
|  | @ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|         questionEntity.content = question.content; |         questionEntity.content = question.content; | ||||||
|         return this.insert(questionEntity); |         return this.insert(questionEntity); | ||||||
|     } |     } | ||||||
|     public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { |     public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { | ||||||
|         return this.findAll({ |         return this.findAll({ | ||||||
|             where: { |             where: { | ||||||
|                 learningObjectHruid: loId.hruid, |                 learningObjectHruid: loId.hruid, | ||||||
|  | @ -33,7 +33,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|             }, |             }, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> { |     public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> { | ||||||
|         return this.deleteWhere({ |         return this.deleteWhere({ | ||||||
|             learningObjectHruid: loId.hruid, |             learningObjectHruid: loId.hruid, | ||||||
|             learningObjectLanguage: loId.language, |             learningObjectLanguage: loId.language, | ||||||
|  |  | ||||||
|  | @ -34,8 +34,8 @@ let entityManager: EntityManager | undefined; | ||||||
| /** | /** | ||||||
|  * Execute all the database operations within the function f in a single transaction. |  * Execute all the database operations within the function f in a single transaction. | ||||||
|  */ |  */ | ||||||
| export function transactional<T>(f: () => Promise<T>) { | export async function transactional<T>(f: () => Promise<T>): Promise<void> { | ||||||
|     entityManager?.transactional(f); |     await entityManager?.transactional(f); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R { | function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R { | ||||||
|  |  | ||||||
|  | @ -1,14 +1,11 @@ | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| // Import { UserRepository } from './user-repository.js';
 |  | ||||||
| 
 |  | ||||||
| // Export class StudentRepository extends UserRepository<Student> {}
 |  | ||||||
| 
 | 
 | ||||||
| export class StudentRepository extends DwengoEntityRepository<Student> { | export class StudentRepository extends DwengoEntityRepository<Student> { | ||||||
|     public findByUsername(username: string): Promise<Student | null> { |     public async findByUsername(username: string): Promise<Student | null> { | ||||||
|         return this.findOne({ username: username }); |         return this.findOne({ username: username }); | ||||||
|     } |     } | ||||||
|     public deleteByUsername(username: string): Promise<void> { |     public async deleteByUsername(username: string): Promise<void> { | ||||||
|         return this.deleteWhere({ username: username }); |         return this.deleteWhere({ username: username }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,10 +2,10 @@ import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| 
 | 
 | ||||||
| export class TeacherRepository extends DwengoEntityRepository<Teacher> { | export class TeacherRepository extends DwengoEntityRepository<Teacher> { | ||||||
|     public findByUsername(username: string): Promise<Teacher | null> { |     public async findByUsername(username: string): Promise<Teacher | null> { | ||||||
|         return this.findOne({ username: username }); |         return this.findOne({ username: username }); | ||||||
|     } |     } | ||||||
|     public deleteByUsername(username: string): Promise<void> { |     public async deleteByUsername(username: string): Promise<void> { | ||||||
|         return this.deleteWhere({ username: username }); |         return this.deleteWhere({ username: username }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| import { User } from '../../entities/users/user.entity.js'; | import { User } from '../../entities/users/user.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class UserRepository<T extends User> extends DwengoEntityRepository<T> { | export class UserRepository<T extends User> extends DwengoEntityRepository<T> { | ||||||
|     public findByUsername(username: string): Promise<T | null> { |     public async findByUsername(username: string): Promise<T | null> { | ||||||
|         return this.findOne({ username } as Partial<T>); |         return this.findOne({ username } as Partial<T>); | ||||||
|     } |     } | ||||||
|     public deleteByUsername(username: string): Promise<void> { |     public async deleteByUsername(username: string): Promise<void> { | ||||||
|         return this.deleteWhere({ username } as Partial<T>); |         return this.deleteWhere({ username } as Partial<T>); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ export class Submission { | ||||||
|     learningObjectLanguage!: Language; |     learningObjectLanguage!: Language; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'numeric' }) |     @PrimaryKey({ type: 'numeric' }) | ||||||
|     learningObjectVersion: number = 1; |     learningObjectVersion = 1; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|     submissionNumber?: number; |     submissionNumber?: number; | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								backend/src/entities/content/educational-goal.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/entities/content/educational-goal.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { Embeddable, Property } from '@mikro-orm/core'; | ||||||
|  | 
 | ||||||
|  | @Embeddable() | ||||||
|  | export class EducationalGoal { | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     source!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     id!: string; | ||||||
|  | } | ||||||
|  | @ -5,5 +5,7 @@ export class LearningObjectIdentifier { | ||||||
|         public hruid: string, |         public hruid: string, | ||||||
|         public language: Language, |         public language: Language, | ||||||
|         public version: number |         public version: number | ||||||
|     ) {} |     ) { | ||||||
|  |         // Do nothing
 | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,28 +1,12 @@ | ||||||
| import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||||
| import { Language } from './language.js'; | import { Language } from './language.js'; | ||||||
| import { Attachment } from './attachment.entity.js'; | import { Attachment } from './attachment.entity.js'; | ||||||
| import { Teacher } from '../users/teacher.entity.js'; | import { Teacher } from '../users/teacher.entity.js'; | ||||||
| import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; | import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; | ||||||
| import { v4 } from 'uuid'; | import { v4 } from 'uuid'; | ||||||
| import { LearningObjectRepository } from '../../data/content/learning-object-repository.js'; | import { LearningObjectRepository } from '../../data/content/learning-object-repository.js'; | ||||||
| 
 | import { EducationalGoal } from './educational-goal.entity.js'; | ||||||
| @Embeddable() | import { ReturnValue } from './return-value.entity.js'; | ||||||
| export class EducationalGoal { |  | ||||||
|     @Property({ type: 'string' }) |  | ||||||
|     source!: string; |  | ||||||
| 
 |  | ||||||
|     @Property({ type: 'string' }) |  | ||||||
|     id!: string; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @Embeddable() |  | ||||||
| export class ReturnValue { |  | ||||||
|     @Property({ type: 'string' }) |  | ||||||
|     callbackUrl!: string; |  | ||||||
| 
 |  | ||||||
|     @Property({ type: 'json' }) |  | ||||||
|     callbackSchema!: string; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| @Entity({ repository: () => LearningObjectRepository }) | @Entity({ repository: () => LearningObjectRepository }) | ||||||
| export class LearningObject { | export class LearningObject { | ||||||
|  | @ -36,7 +20,7 @@ export class LearningObject { | ||||||
|     language!: Language; |     language!: Language; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'number' }) |     @PrimaryKey({ type: 'number' }) | ||||||
|     version: number = 1; |     version = 1; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'uuid', unique: true }) |     @Property({ type: 'uuid', unique: true }) | ||||||
|     uuid = v4(); |     uuid = v4(); | ||||||
|  | @ -62,7 +46,7 @@ export class LearningObject { | ||||||
|     targetAges?: number[] = []; |     targetAges?: number[] = []; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'bool' }) |     @Property({ type: 'bool' }) | ||||||
|     teacherExclusive: boolean = false; |     teacherExclusive = false; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'array' }) |     @Property({ type: 'array' }) | ||||||
|     skosConcepts: string[] = []; |     skosConcepts: string[] = []; | ||||||
|  | @ -74,10 +58,10 @@ export class LearningObject { | ||||||
|     educationalGoals: EducationalGoal[] = []; |     educationalGoals: EducationalGoal[] = []; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     copyright: string = ''; |     copyright = ''; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     license: string = ''; |     license = ''; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'smallint', nullable: true }) |     @Property({ type: 'smallint', nullable: true }) | ||||||
|     difficulty?: number; |     difficulty?: number; | ||||||
|  | @ -91,7 +75,7 @@ export class LearningObject { | ||||||
|     returnValue!: ReturnValue; |     returnValue!: ReturnValue; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'bool' }) |     @Property({ type: 'bool' }) | ||||||
|     available: boolean = true; |     available = true; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'string', nullable: true }) |     @Property({ type: 'string', nullable: true }) | ||||||
|     contentLocation?: string; |     contentLocation?: string; | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								backend/src/entities/content/return-value.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/entities/content/return-value.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { Embeddable, Property } from '@mikro-orm/core'; | ||||||
|  | 
 | ||||||
|  | @Embeddable() | ||||||
|  | export class ReturnValue { | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     callbackUrl!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'json' }) | ||||||
|  |     callbackSchema!: string; | ||||||
|  | } | ||||||
|  | @ -15,7 +15,7 @@ export class Question { | ||||||
|     learningObjectLanguage!: Language; |     learningObjectLanguage!: Language; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'number' }) |     @PrimaryKey({ type: 'number' }) | ||||||
|     learningObjectVersion: number = 1; |     learningObjectVersion = 1; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|     sequenceNumber?: number; |     sequenceNumber?: number; | ||||||
|  |  | ||||||
|  | @ -13,12 +13,4 @@ export class Student extends User { | ||||||
| 
 | 
 | ||||||
|     @ManyToMany(() => Group) |     @ManyToMany(() => Group) | ||||||
|     groups!: Collection<Group>; |     groups!: Collection<Group>; | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         public username: string, |  | ||||||
|         public firstName: string, |  | ||||||
|         public lastName: string |  | ||||||
|     ) { |  | ||||||
|         super(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,12 +7,4 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js'; | ||||||
| export class Teacher extends User { | export class Teacher extends User { | ||||||
|     @ManyToMany(() => Class) |     @ManyToMany(() => Class) | ||||||
|     classes!: Collection<Class>; |     classes!: Collection<Class>; | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         public username: string, |  | ||||||
|         public firstName: string, |  | ||||||
|         public lastName: string |  | ||||||
|     ) { |  | ||||||
|         super(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,8 +6,8 @@ export abstract class User { | ||||||
|     username!: string; |     username!: string; | ||||||
| 
 | 
 | ||||||
|     @Property() |     @Property() | ||||||
|     firstName: string = ''; |     firstName = ''; | ||||||
| 
 | 
 | ||||||
|     @Property() |     @Property() | ||||||
|     lastName: string = ''; |     lastName = ''; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,42 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Exception for HTTP 400 Bad Request |  | ||||||
|  */ |  | ||||||
| export class BadRequestException extends Error { |  | ||||||
|     public status = 400; |  | ||||||
| 
 |  | ||||||
|     constructor(error: string) { |  | ||||||
|         super(error); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 401 Unauthorized |  | ||||||
|  */ |  | ||||||
| export class UnauthorizedException extends Error { |  | ||||||
|     status = 401; |  | ||||||
|     constructor(message: string = 'Unauthorized') { |  | ||||||
|         super(message); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 403 Forbidden |  | ||||||
|  */ |  | ||||||
| export class ForbiddenException extends Error { |  | ||||||
|     status = 403; |  | ||||||
| 
 |  | ||||||
|     constructor(message: string = 'Forbidden') { |  | ||||||
|         super(message); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 404 Not Found |  | ||||||
|  */ |  | ||||||
| export class NotFoundException extends Error { |  | ||||||
|     public status = 404; |  | ||||||
| 
 |  | ||||||
|     constructor(error: string) { |  | ||||||
|         super(error); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 400 Bad Request | ||||||
|  |  */ | ||||||
|  | export class BadRequestException extends ExceptionWithHttpState { | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(400, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 409 Conflict | ||||||
|  |  */ | ||||||
|  | export class ConflictException extends ExceptionWithHttpState { | ||||||
|  |     public status = 409; | ||||||
|  | 
 | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(409, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | import { ConflictException } from './conflict-exception.js'; | ||||||
|  | 
 | ||||||
|  | export class EntityAlreadyExistsException extends ConflictException { | ||||||
|  |     constructor(message: string) { | ||||||
|  |         super(message); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | /** | ||||||
|  |  * Exceptions which are associated with a HTTP error code. | ||||||
|  |  */ | ||||||
|  | export abstract class ExceptionWithHttpState extends Error { | ||||||
|  |     constructor( | ||||||
|  |         public status: number, | ||||||
|  |         public error: string | ||||||
|  |     ) { | ||||||
|  |         super(error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 403 Forbidden | ||||||
|  |  */ | ||||||
|  | export class ForbiddenException extends ExceptionWithHttpState { | ||||||
|  |     status = 403; | ||||||
|  | 
 | ||||||
|  |     constructor(message = 'Forbidden') { | ||||||
|  |         super(403, message); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 404 Not Found | ||||||
|  |  */ | ||||||
|  | export class NotFoundException extends ExceptionWithHttpState { | ||||||
|  |     public status = 404; | ||||||
|  | 
 | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(404, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 401 Unauthorized | ||||||
|  |  */ | ||||||
|  | export class UnauthorizedException extends ExceptionWithHttpState { | ||||||
|  |     constructor(message = 'Unauthorized') { | ||||||
|  |         super(401, message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -3,6 +3,7 @@ import { Assignment } from '../entities/assignments/assignment.entity.js'; | ||||||
| import { Class } from '../entities/classes/class.entity.js'; | import { Class } from '../entities/classes/class.entity.js'; | ||||||
| import { languageMap } from '../entities/content/language.js'; | import { languageMap } from '../entities/content/language.js'; | ||||||
| import { GroupDTO } from './group.js'; | import { GroupDTO } from './group.js'; | ||||||
|  | import { getLogger } from '../logging/initalize.js'; | ||||||
| 
 | 
 | ||||||
| export interface AssignmentDTO { | export interface AssignmentDTO { | ||||||
|     id: number; |     id: number; | ||||||
|  | @ -46,5 +47,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi | ||||||
|     assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; |     assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; | ||||||
|     assignment.within = cls; |     assignment.within = cls; | ||||||
| 
 | 
 | ||||||
|  |     getLogger().debug(assignment); | ||||||
|  | 
 | ||||||
|     return assignment; |     return assignment; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -58,7 +58,7 @@ export interface EducationalGoal { | ||||||
| 
 | 
 | ||||||
| export interface ReturnValue { | export interface ReturnValue { | ||||||
|     callback_url: string; |     callback_url: string; | ||||||
|     callback_schema: Record<string, any>; |     callback_schema: Record<string, unknown>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface LearningObjectMetadata { | export interface LearningObjectMetadata { | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { Student } from '../entities/users/student.entity.js'; | import { Student } from '../entities/users/student.entity.js'; | ||||||
|  | import { getStudentRepository } from '../data/repositories.js'; | ||||||
| 
 | 
 | ||||||
| export interface StudentDTO { | export interface StudentDTO { | ||||||
|     id: string; |     id: string; | ||||||
|  | @ -23,7 +24,9 @@ export function mapToStudentDTO(student: Student): StudentDTO { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToStudent(studentData: StudentDTO): Student { | export function mapToStudent(studentData: StudentDTO): Student { | ||||||
|     const student = new Student(studentData.username, studentData.firstName, studentData.lastName); |     return getStudentRepository().create({ | ||||||
| 
 |         username: studentData.username, | ||||||
|     return student; |         firstName: studentData.firstName, | ||||||
|  |         lastName: studentData.lastName, | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { Teacher } from '../entities/users/teacher.entity.js'; | import { Teacher } from '../entities/users/teacher.entity.js'; | ||||||
|  | import { getTeacherRepository } from '../data/repositories.js'; | ||||||
| 
 | 
 | ||||||
| export interface TeacherDTO { | export interface TeacherDTO { | ||||||
|     id: string; |     id: string; | ||||||
|  | @ -22,8 +23,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToTeacher(TeacherData: TeacherDTO): Teacher { | export function mapToTeacher(teacherData: TeacherDTO): Teacher { | ||||||
|     const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName); |     return getTeacherRepository().create({ | ||||||
| 
 |         username: teacherData.username, | ||||||
|     return teacher; |         firstName: teacherData.firstName, | ||||||
|  |         lastName: teacherData.lastName, | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; | import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; | ||||||
| import LokiTransport from 'winston-loki'; | import LokiTransport from 'winston-loki'; | ||||||
| import { LokiLabels } from 'loki-logger-ts'; | import { LokiLabels } from 'loki-logger-ts'; | ||||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | import { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
| 
 | 
 | ||||||
| export class Logger extends WinstonLogger { | export class Logger extends WinstonLogger { | ||||||
|     constructor() { |     constructor() { | ||||||
|  | @ -9,7 +9,7 @@ export class Logger extends WinstonLogger { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const Labels: LokiLabels = { | const lokiLabels: LokiLabels = { | ||||||
|     source: 'Dwengo-Backend', |     source: 'Dwengo-Backend', | ||||||
|     service: 'API', |     service: 'API', | ||||||
|     host: 'localhost', |     host: 'localhost', | ||||||
|  | @ -22,28 +22,28 @@ function initializeLogger(): Logger { | ||||||
|         return logger; |         return logger; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const logLevel = getEnvVar(EnvVars.LogLevel); |     const logLevel = getEnvVar(envVars.LogLevel); | ||||||
| 
 | 
 | ||||||
|     const consoleTransport = new transports.Console({ |     const consoleTransport = new transports.Console({ | ||||||
|         level: getEnvVar(EnvVars.LogLevel), |         level: getEnvVar(envVars.LogLevel), | ||||||
|         format: format.combine(format.cli(), format.colorize()), |         format: format.combine(format.cli(), format.colorize()), | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (getEnvVar(EnvVars.RunMode) === 'dev') { |     if (getEnvVar(envVars.RunMode) === 'dev') { | ||||||
|         return createLogger({ |         return createLogger({ | ||||||
|             transports: [consoleTransport], |             transports: [consoleTransport], | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const lokiHost = getEnvVar(EnvVars.LokiHost); |     const lokiHost = getEnvVar(envVars.LokiHost); | ||||||
| 
 | 
 | ||||||
|     const lokiTransport: LokiTransport = new LokiTransport({ |     const lokiTransport: LokiTransport = new LokiTransport({ | ||||||
|         host: lokiHost, |         host: lokiHost, | ||||||
|         labels: Labels, |         labels: lokiLabels, | ||||||
|         level: logLevel, |         level: logLevel, | ||||||
|         json: true, |         json: true, | ||||||
|         format: format.combine(format.timestamp(), format.json()), |         format: format.combine(format.timestamp(), format.json()), | ||||||
|         onConnectionError: (err) => { |         onConnectionError: (err): void => { | ||||||
|             // eslint-disable-next-line no-console
 |             // eslint-disable-next-line no-console
 | ||||||
|             console.error(`Connection error: ${err}`); |             console.error(`Connection error: ${err}`); | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  | @ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts'; | ||||||
| export class MikroOrmLogger extends DefaultLogger { | export class MikroOrmLogger extends DefaultLogger { | ||||||
|     private logger: Logger = getLogger(); |     private logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|     log(namespace: LoggerNamespace, message: string, context?: LogContext) { |     static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown { | ||||||
|  |         const labels: LokiLabels = { | ||||||
|  |             service: 'ORM', | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let message: string; | ||||||
|  |         if (context?.label) { | ||||||
|  |             message = `[${namespace}] (${context.label}) ${messageArg}`; | ||||||
|  |         } else { | ||||||
|  |             message = `[${namespace}] ${messageArg}`; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             message: message, | ||||||
|  |             labels: labels, | ||||||
|  |             context: context, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     log(namespace: LoggerNamespace, message: string, context?: LogContext): void { | ||||||
|         if (!this.isEnabled(namespace, context)) { |         if (!this.isEnabled(namespace, context)) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         switch (namespace) { |         switch (namespace) { | ||||||
|             case 'query': |             case 'query': | ||||||
|                 this.logger.debug(this.createMessage(namespace, message, context)); |                 this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'query-params': |             case 'query-params': | ||||||
|                 // TODO Which log level should this be?
 |                 // TODO Which log level should this be?
 | ||||||
|                 this.logger.info(this.createMessage(namespace, message, context)); |                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'schema': |             case 'schema': | ||||||
|                 this.logger.info(this.createMessage(namespace, message, context)); |                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'discovery': |             case 'discovery': | ||||||
|                 this.logger.debug(this.createMessage(namespace, message, context)); |                 this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'info': |             case 'info': | ||||||
|                 this.logger.info(this.createMessage(namespace, message, context)); |                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'deprecated': |             case 'deprecated': | ||||||
|                 this.logger.warn(this.createMessage(namespace, message, context)); |                 this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             default: |             default: | ||||||
|                 switch (context?.level) { |                 switch (context?.level) { | ||||||
|                     case 'info': |                     case 'info': | ||||||
|                         this.logger.info(this.createMessage(namespace, message, context)); |                         this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                         break; |                         break; | ||||||
|                     case 'warning': |                     case 'warning': | ||||||
|                         this.logger.warn(message); |                         this.logger.warn(message); | ||||||
|  | @ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger { | ||||||
|                 } |                 } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) { |  | ||||||
|         const labels: LokiLabels = { |  | ||||||
|             service: 'ORM', |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         let message: string; |  | ||||||
|         if (context?.label) { |  | ||||||
|             message = `[${namespace}] (${context?.label}) ${messageArg}`; |  | ||||||
|         } else { |  | ||||||
|             message = `[${namespace}] ${messageArg}`; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return { |  | ||||||
|             message: message, |  | ||||||
|             labels: labels, |  | ||||||
|             context: context, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { getLogger, Logger } from './initalize.js'; | import { getLogger, Logger } from './initalize.js'; | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| 
 | 
 | ||||||
| export function responseTimeLogger(req: Request, res: Response, time: number) { | export function responseTimeLogger(req: Request, res: Response, time: number): void { | ||||||
|     const logger: Logger = getLogger(); |     const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|     const method = req.method; |     const method = req.method; | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| import { EnvVars, getEnvVar } from '../../util/envvars.js'; | import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||||
| import { expressjwt } from 'express-jwt'; | import { expressjwt } from 'express-jwt'; | ||||||
|  | import * as jwt from 'jsonwebtoken'; | ||||||
| import { JwtPayload } from 'jsonwebtoken'; | import { JwtPayload } from 'jsonwebtoken'; | ||||||
| import jwksClient from 'jwks-rsa'; | import jwksClient from 'jwks-rsa'; | ||||||
| import * as express from 'express'; | import * as express from 'express'; | ||||||
| import * as jwt from 'jsonwebtoken'; |  | ||||||
| import { AuthenticatedRequest } from './authenticated-request.js'; | import { AuthenticatedRequest } from './authenticated-request.js'; | ||||||
| import { AuthenticationInfo } from './authentication-info.js'; | import { AuthenticationInfo } from './authentication-info.js'; | ||||||
| import { ForbiddenException, UnauthorizedException } from '../../exceptions.js'; | import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; | ||||||
|  | import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; | ||||||
| 
 | 
 | ||||||
| const JWKS_CACHE = true; | const JWKS_CACHE = true; | ||||||
| const JWKS_RATE_LIMIT = true; | const JWKS_RATE_LIMIT = true; | ||||||
|  | @ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient { | ||||||
| 
 | 
 | ||||||
| const idpConfigs = { | const idpConfigs = { | ||||||
|     student: { |     student: { | ||||||
|         issuer: getEnvVar(EnvVars.IdpStudentUrl), |         issuer: getEnvVar(envVars.IdpStudentUrl), | ||||||
|         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), |         jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)), | ||||||
|     }, |     }, | ||||||
|     teacher: { |     teacher: { | ||||||
|         issuer: getEnvVar(EnvVars.IdpTeacherUrl), |         issuer: getEnvVar(envVars.IdpTeacherUrl), | ||||||
|         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), |         jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)), | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({ | ||||||
|         } |         } | ||||||
|         return signingKey.getPublicKey(); |         return signingKey.getPublicKey(); | ||||||
|     }, |     }, | ||||||
|     audience: getEnvVar(EnvVars.IdpAudience), |     audience: getEnvVar(envVars.IdpAudience), | ||||||
|     algorithms: [JWT_ALGORITHM], |     algorithms: [JWT_ALGORITHM], | ||||||
|     credentialsRequired: false, |     credentialsRequired: false, | ||||||
|     requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, |     requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, | ||||||
|  | @ -74,7 +75,7 @@ const verifyJwtToken = expressjwt({ | ||||||
|  */ |  */ | ||||||
| function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { | function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { | ||||||
|     if (!req.jwtPayload) { |     if (!req.jwtPayload) { | ||||||
|         return; |         return undefined; | ||||||
|     } |     } | ||||||
|     const issuer = req.jwtPayload.iss; |     const issuer = req.jwtPayload.iss; | ||||||
|     let accountType: 'student' | 'teacher'; |     let accountType: 'student' | 'teacher'; | ||||||
|  | @ -84,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | | ||||||
|     } else if (issuer === idpConfigs.teacher.issuer) { |     } else if (issuer === idpConfigs.teacher.issuer) { | ||||||
|         accountType = 'teacher'; |         accountType = 'teacher'; | ||||||
|     } else { |     } else { | ||||||
|         return; |         return undefined; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|         accountType: accountType, |         accountType: accountType, | ||||||
|         username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, |         username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, | ||||||
|  | @ -100,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | | ||||||
|  * Add the AuthenticationInfo object with the information about the current authentication to the request in order |  * Add the AuthenticationInfo object with the information about the current authentication to the request in order | ||||||
|  * to avoid that the routers have to deal with the JWT token. |  * to avoid that the routers have to deal with the JWT token. | ||||||
|  */ |  */ | ||||||
| const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => { | function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void { | ||||||
|     req.auth = getAuthenticationInfo(req); |     req.auth = getAuthenticationInfo(req); | ||||||
|     next(); |     next(); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | ||||||
| 
 | 
 | ||||||
|  | @ -113,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | ||||||
|  * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates |  * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates | ||||||
|  *                        to true. |  *                        to true. | ||||||
|  */ |  */ | ||||||
| export const authorize = | export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) { | ||||||
|     (accessCondition: (auth: AuthenticationInfo) => boolean) => |     return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => { | ||||||
|     (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => { |  | ||||||
|         if (!req.auth) { |         if (!req.auth) { | ||||||
|             throw new UnauthorizedException(); |             throw new UnauthorizedException(); | ||||||
|         } else if (!accessCondition(req.auth)) { |         } else if (!accessCondition(req.auth)) { | ||||||
|  | @ -124,6 +125,7 @@ export const authorize = | ||||||
|             next(); |             next(); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Middleware which rejects all unauthenticated users, but accepts all authenticated users. |  * Middleware which rejects all unauthenticated users, but accepts all authenticated users. | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| /** | /** | ||||||
|  * Object with information about the user who is currently logged in. |  * Object with information about the user who is currently logged in. | ||||||
|  */ |  */ | ||||||
| export type AuthenticationInfo = { | export interface AuthenticationInfo { | ||||||
|     accountType: 'student' | 'teacher'; |     accountType: 'student' | 'teacher'; | ||||||
|     username: string; |     username: string; | ||||||
|     name?: string; |     name?: string; | ||||||
|     firstName?: string; |     firstName?: string; | ||||||
|     lastName?: string; |     lastName?: string; | ||||||
|     email?: string; |     email?: string; | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import cors from 'cors'; | import cors from 'cors'; | ||||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | import { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
| 
 | 
 | ||||||
| export default cors({ | export default cors({ | ||||||
|     origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','), |     origin: getEnvVar(envVars.CorsAllowedOrigins).split(','), | ||||||
|     allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','), |     allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','), | ||||||
| }); | }); | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | import { NextFunction, Request, Response } from 'express'; | ||||||
|  | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
|  | import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | const logger: Logger = getLogger(); | ||||||
|  | 
 | ||||||
|  | export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void { | ||||||
|  |     if (err instanceof ExceptionWithHttpState) { | ||||||
|  |         logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); | ||||||
|  |         res.status(err.status).json(err); | ||||||
|  |     } else { | ||||||
|  |         logger.error(`Unexpected error occurred while handing a request: ${JSON.stringify(err)}`); | ||||||
|  |         res.status(500).json(err); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { LoggerOptions, Options } from '@mikro-orm/core'; | import { LoggerOptions, Options } from '@mikro-orm/core'; | ||||||
| import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | ||||||
| import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; | import { envVars, getEnvVar, getNumericEnvVar } from './util/envVars.js'; | ||||||
| import { SqliteDriver } from '@mikro-orm/sqlite'; | import { SqliteDriver } from '@mikro-orm/sqlite'; | ||||||
| import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | ||||||
| 
 | 
 | ||||||
|  | @ -42,33 +42,35 @@ const entities = [ | ||||||
|     Question, |     Question, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| function config(testingMode: boolean = false): Options { | function config(testingMode = false): Options { | ||||||
|     if (testingMode) { |     if (testingMode) { | ||||||
|         return { |         return { | ||||||
|             driver: SqliteDriver, |             driver: SqliteDriver, | ||||||
|             dbName: getEnvVar(EnvVars.DbName), |             dbName: getEnvVar(envVars.DbName), | ||||||
|             subscribers: [new SqliteAutoincrementSubscriber()], |             subscribers: [new SqliteAutoincrementSubscriber()], | ||||||
|             entities: entities, |             entities: entities, | ||||||
|  |             persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
 | ||||||
|             // EntitiesTs: entitiesTs,
 |             // EntitiesTs: entitiesTs,
 | ||||||
| 
 | 
 | ||||||
|             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 |             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 | ||||||
|             // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
 |             // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
 | ||||||
|             dynamicImportProvider: (id) => import(id), |             dynamicImportProvider: async (id) => import(id), | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         driver: PostgreSqlDriver, |         driver: PostgreSqlDriver, | ||||||
|         host: getEnvVar(EnvVars.DbHost), |         host: getEnvVar(envVars.DbHost), | ||||||
|         port: getNumericEnvVar(EnvVars.DbPort), |         port: getNumericEnvVar(envVars.DbPort), | ||||||
|         dbName: getEnvVar(EnvVars.DbName), |         dbName: getEnvVar(envVars.DbName), | ||||||
|         user: getEnvVar(EnvVars.DbUsername), |         user: getEnvVar(envVars.DbUsername), | ||||||
|         password: getEnvVar(EnvVars.DbPassword), |         password: getEnvVar(envVars.DbPassword), | ||||||
|         entities: entities, |         entities: entities, | ||||||
|  |         persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
 | ||||||
|         // EntitiesTs: entitiesTs,
 |         // EntitiesTs: entitiesTs,
 | ||||||
| 
 | 
 | ||||||
|         // Logging
 |         // Logging
 | ||||||
|         debug: getEnvVar(EnvVars.LogLevel) === 'debug', |         debug: getEnvVar(envVars.LogLevel) === 'debug', | ||||||
|         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), |         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { EntityManager, MikroORM } from '@mikro-orm/core'; | import { EntityManager, MikroORM } from '@mikro-orm/core'; | ||||||
| import config from './mikro-orm.config.js'; | import config from './mikro-orm.config.js'; | ||||||
| import { EnvVars, getEnvVar } from './util/envvars.js'; | import { envVars, getEnvVar } from './util/envVars.js'; | ||||||
| import { getLogger, Logger } from './logging/initalize.js'; | import { getLogger, Logger } from './logging/initalize.js'; | ||||||
| 
 | 
 | ||||||
| let orm: MikroORM | undefined; | let orm: MikroORM | undefined; | ||||||
| export async function initORM(testingMode: boolean = false) { | export async function initORM(testingMode = false): Promise<void> { | ||||||
|     const logger: Logger = getLogger(); |     const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|     logger.info('Initializing ORM'); |     logger.info('Initializing ORM'); | ||||||
|  | @ -12,7 +12,7 @@ export async function initORM(testingMode: boolean = false) { | ||||||
| 
 | 
 | ||||||
|     orm = await MikroORM.init(config(testingMode)); |     orm = await MikroORM.init(config(testingMode)); | ||||||
|     // Update the database scheme if necessary and enabled.
 |     // Update the database scheme if necessary and enabled.
 | ||||||
|     if (getEnvVar(EnvVars.DbUpdate)) { |     if (getEnvVar(envVars.DbUpdate)) { | ||||||
|         await orm.schema.updateSchema(); |         await orm.schema.updateSchema(); | ||||||
|     } else { |     } else { | ||||||
|         const diff = await orm.schema.getUpdateSchemaSQL(); |         const diff = await orm.schema.getUpdateSchemaSQL(); | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ router.get('/:id', getAssignmentHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/submissions', getAssignmentsSubmissionsHandler); | router.get('/:id/submissions', getAssignmentsSubmissionsHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/questions', (req, res) => { | router.get('/:id/questions', (_req, res) => { | ||||||
|     res.json({ |     res.json({ | ||||||
|         questions: ['0'], |         questions: ['0'], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -4,21 +4,21 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Returns auth configuration for frontend
 | // Returns auth configuration for frontend
 | ||||||
| router.get('/config', (req, res) => { | router.get('/config', (_req, res) => { | ||||||
|     res.json(getFrontendAuthConfig()); |     res.json(getFrontendAuthConfig()); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => { | router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { | ||||||
|     /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ |     /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ | ||||||
|     res.json({ message: 'If you see this, you should be authenticated!' }); |     res.json({ message: 'If you see this, you should be authenticated!' }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| router.get('/testStudentsOnly', studentsOnly, (req, res) => { | router.get('/testStudentsOnly', studentsOnly, (_req, res) => { | ||||||
|     /* #swagger.security = [{ "student": [ ] }] */ |     /* #swagger.security = [{ "student": [ ] }] */ | ||||||
|     res.json({ message: 'If you see this, you should be a student!' }); |     res.json({ message: 'If you see this, you should be a student!' }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| router.get('/testTeachersOnly', teachersOnly, (req, res) => { | router.get('/testTeachersOnly', teachersOnly, (_req, res) => { | ||||||
|     /* #swagger.security = [{ "teacher": [ ] }] */ |     /* #swagger.security = [{ "teacher": [ ] }] */ | ||||||
|     res.json({ message: 'If you see this, you should be a teacher!' }); |     res.json({ message: 'If you see this, you should be a teacher!' }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ router.get('/:groupid', getGroupHandler); | ||||||
| router.get('/:groupid', getGroupSubmissionsHandler); | router.get('/:groupid', getGroupSubmissionsHandler); | ||||||
| 
 | 
 | ||||||
| // The list of questions a group has made
 | // The list of questions a group has made
 | ||||||
| router.get('/:id/questions', (req, res) => { | router.get('/:id/questions', (_req, res) => { | ||||||
|     res.json({ |     res.json({ | ||||||
|         questions: ['0'], |         questions: ['0'], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import { | ||||||
|     getStudentHandler, |     getStudentHandler, | ||||||
|     getStudentSubmissionsHandler, |     getStudentSubmissionsHandler, | ||||||
| } from '../controllers/students.js'; | } from '../controllers/students.js'; | ||||||
|  | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | // Root endpoint used to search objects
 | ||||||
|  | @ -36,7 +37,7 @@ router.get('/:id/assignments', getStudentAssignmentsHandler); | ||||||
| router.get('/:id/groups', getStudentGroupsHandler); | router.get('/:id/groups', getStudentGroupsHandler); | ||||||
| 
 | 
 | ||||||
| // A list of questions a user has created
 | // A list of questions a user has created
 | ||||||
| router.get('/:id/questions', (req, res) => { | router.get('/:id/questions', (_req, res) => { | ||||||
|     res.json({ |     res.json({ | ||||||
|         questions: ['0'], |         questions: ['0'], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | // Root endpoint used to search objects
 | ||||||
| router.get('/', (req, res) => { | router.get('/', (_req, res) => { | ||||||
|     res.json({ |     res.json({ | ||||||
|         submissions: ['0', '1'], |         submissions: ['0', '1'], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -28,7 +28,7 @@ router.get('/:username/students', getTeacherStudentHandler); | ||||||
| router.get('/:username/questions', getTeacherQuestionHandler); | router.get('/:username/questions', getTeacherQuestionHandler); | ||||||
| 
 | 
 | ||||||
| // Invitations to other classes a teacher received
 | // Invitations to other classes a teacher received
 | ||||||
| router.get('/:id/invitations', (req, res) => { | router.get('/:id/invitations', (_req, res) => { | ||||||
|     res.json({ |     res.json({ | ||||||
|         invitations: ['0'], |         invitations: ['0'], | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; | import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; | ||||||
| import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; | import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; | ||||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | ||||||
|  | import { getLogger } from '../logging/initalize.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> { | export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> { | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|  | @ -37,7 +38,7 @@ export async function createAssignment(classid: string, assignmentData: Assignme | ||||||
| 
 | 
 | ||||||
|         return mapToAssignmentDTO(newAssignment); |         return mapToAssignmentDTO(newAssignment); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|         console.error(e); |         getLogger().error(e); | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -83,7 +84,7 @@ export async function getAssignmentsSubmissions( | ||||||
|     const groups = await groupRepository.findAllGroupsForAssignment(assignment); |     const groups = await groupRepository.findAllGroupsForAssignment(assignment); | ||||||
| 
 | 
 | ||||||
|     const submissionRepository = getSubmissionRepository(); |     const submissionRepository = getSubmissionRepository(); | ||||||
|     const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); |     const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); | ||||||
| 
 | 
 | ||||||
|     if (full) { |     if (full) { | ||||||
|         return submissions.map(mapToSubmissionDTO); |         return submissions.map(mapToSubmissionDTO); | ||||||
|  |  | ||||||
|  | @ -23,11 +23,15 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[ | ||||||
| export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> { | export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository = getTeacherRepository(); | ||||||
|     const teacherUsernames = classData.teachers || []; |     const teacherUsernames = classData.teachers || []; | ||||||
|     const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null); |     const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter( | ||||||
|  |         (teacher) => teacher !== null | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
|     const studentUsernames = classData.students || []; |     const studentUsernames = classData.students || []; | ||||||
|     const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); |     const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter( | ||||||
|  |         (student) => student !== null | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import { | ||||||
| import { Group } from '../entities/assignments/group.entity.js'; | import { Group } from '../entities/assignments/group.entity.js'; | ||||||
| import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | ||||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | ||||||
|  | import { getLogger } from '../logging/initalize.js'; | ||||||
| 
 | 
 | ||||||
| export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> { | export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> { | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|  | @ -42,9 +43,11 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
| 
 | 
 | ||||||
|     const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
 |     const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
 | ||||||
|     const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); |     const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter( | ||||||
|  |         (student) => student !== null | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     console.log(members); |     getLogger().debug(members); | ||||||
| 
 | 
 | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const cls = await classRepository.findById(classid); |     const cls = await classRepository.findById(classid); | ||||||
|  | @ -70,7 +73,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme | ||||||
| 
 | 
 | ||||||
|         return newGroup; |         return newGroup; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|         console.log(e); |         getLogger().error(e); | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -94,8 +97,7 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu | ||||||
|     const groups = await groupRepository.findAllGroupsForAssignment(assignment); |     const groups = await groupRepository.findAllGroupsForAssignment(assignment); | ||||||
| 
 | 
 | ||||||
|     if (full) { |     if (full) { | ||||||
|         console.log('full'); |         getLogger().debug({ full: full, groups: groups }); | ||||||
|         console.log(groups); |  | ||||||
|         return groups.map(mapToGroupDTO); |         return groups.map(mapToGroupDTO); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import { DWENGO_API_BASE } from '../config.js'; | import { DWENGO_API_BASE } from '../config.js'; | ||||||
| import { fetchWithLogging } from '../util/api-helper.js'; | import { fetchWithLogging } from '../util/api-helper.js'; | ||||||
| import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; | import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; | ||||||
|  | import { getLogger } from '../logging/initalize.js'; | ||||||
| 
 | 
 | ||||||
| function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { | function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { | ||||||
|     return { |     return { | ||||||
|  | @ -37,7 +38,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     if (!metadata) { |     if (!metadata) { | ||||||
|         console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); |         getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`); | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -48,7 +49,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr | ||||||
| /** | /** | ||||||
|  * Generic function to fetch learning paths |  * Generic function to fetch learning paths | ||||||
|  */ |  */ | ||||||
| function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> { | function fetchLearningPaths(_arg0: string[], _language: string, _arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> { | ||||||
|     throw new Error('Function not implemented.'); |     throw new Error('Function not implemented.'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -60,7 +61,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri | ||||||
|         const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`); |         const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`); | ||||||
| 
 | 
 | ||||||
|         if (!learningPathResponse.success || !learningPathResponse.data?.length) { |         if (!learningPathResponse.success || !learningPathResponse.data?.length) { | ||||||
|             console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); |             getLogger().error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); | ||||||
|             return []; |             return []; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -74,7 +75,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri | ||||||
|             objects.filter((obj): obj is FilteredLearningObject => obj !== null) |             objects.filter((obj): obj is FilteredLearningObject => obj !== null) | ||||||
|         ); |         ); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|         console.error('❌ Error fetching learning objects:', error); |         getLogger().error('❌ Error fetching learning objects:', error); | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { Attachment } from '../../entities/content/attachment.entity.js'; | ||||||
| import { LearningObjectIdentifier } from '../../interfaces/learning-content.js'; | import { LearningObjectIdentifier } from '../../interfaces/learning-content.js'; | ||||||
| 
 | 
 | ||||||
| const attachmentService = { | const attachmentService = { | ||||||
|     getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { |     async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { | ||||||
|         const attachmentRepo = getAttachmentRepository(); |         const attachmentRepo = getAttachmentRepository(); | ||||||
| 
 | 
 | ||||||
|         if (learningObjectId.version) { |         if (learningObjectId.version) { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import { LearningObjectProvider } from './learning-object-provider.js'; | import { LearningObjectProvider } from './learning-object-provider.js'; | ||||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; | import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; | ||||||
| import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js'; | import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js'; | ||||||
| import { Language } from '../../entities/content/language.js'; |  | ||||||
| import { LearningObject } from '../../entities/content/learning-object.entity.js'; | import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||||
| import { getUrlStringForLearningObject } from '../../util/links.js'; | import { getUrlStringForLearningObject } from '../../util/links.js'; | ||||||
| import processingService from './processing/processing-service.js'; | import processingService from './processing/processing-service.js'; | ||||||
|  | @ -41,10 +40,10 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> { | async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> { | ||||||
|     const learningObjectRepo = getLearningObjectRepository(); |     const learningObjectRepo = getLearningObjectRepository(); | ||||||
| 
 | 
 | ||||||
|     return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); |     return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -65,11 +64,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | ||||||
|     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { |     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { | ||||||
|         const learningObjectRepo = getLearningObjectRepository(); |         const learningObjectRepo = getLearningObjectRepository(); | ||||||
| 
 | 
 | ||||||
|         const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); |         const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); | ||||||
|         if (!learningObject) { |         if (!learningObject) { | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|         return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id)); |         return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id)); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -96,7 +95,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | ||||||
|             throw new NotFoundError('The learning path with the given ID could not be found.'); |             throw new NotFoundError('The learning path with the given ID could not be found.'); | ||||||
|         } |         } | ||||||
|         const learningObjects = await Promise.all( |         const learningObjects = await Promise.all( | ||||||
|             learningPath.nodes.map((it) => { |             learningPath.nodes.map(async (it) => { | ||||||
|                 const learningObject = learningObjectService.getLearningObjectById({ |                 const learningObject = learningObjectService.getLearningObjectById({ | ||||||
|                     hruid: it.learningObjectHruid, |                     hruid: it.learningObjectHruid, | ||||||
|                     language: it.language, |                     language: it.language, | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; | import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; | ||||||
| import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js'; | import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js'; | ||||||
| import { LearningObjectProvider } from './learning-object-provider.js'; | import { LearningObjectProvider } from './learning-object-provider.js'; | ||||||
| import { EnvVars, getEnvVar } from '../../util/envvars.js'; | import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||||
| import databaseLearningObjectProvider from './database-learning-object-provider.js'; | import databaseLearningObjectProvider from './database-learning-object-provider.js'; | ||||||
| 
 | 
 | ||||||
| function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { | function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { | ||||||
|     if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { |     if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { | ||||||
|         return databaseLearningObjectProvider; |         return databaseLearningObjectProvider; | ||||||
|     } |     } | ||||||
|     return dwengoApiLearningObjectProvider; |     return dwengoApiLearningObjectProvider; | ||||||
|  | @ -18,28 +18,28 @@ const learningObjectService = { | ||||||
|     /** |     /** | ||||||
|      * Fetches a single learning object by its HRUID |      * Fetches a single learning object by its HRUID | ||||||
|      */ |      */ | ||||||
|     getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { |     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { | ||||||
|         return getProvider(id).getLearningObjectById(id); |         return getProvider(id).getLearningObjectById(id); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch full learning object data (metadata) |      * Fetch full learning object data (metadata) | ||||||
|      */ |      */ | ||||||
|     getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { |     async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { | ||||||
|         return getProvider(id).getLearningObjectsFromPath(id); |         return getProvider(id).getLearningObjectsFromPath(id); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Fetch only learning object HRUIDs |      * Fetch only learning object HRUIDs | ||||||
|      */ |      */ | ||||||
|     getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> { |     async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> { | ||||||
|         return getProvider(id).getLearningObjectIdsFromPath(id); |         return getProvider(id).getLearningObjectIdsFromPath(id); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). |      * Obtain a HTML-rendering of the learning object with the given identifier (as a string). | ||||||
|      */ |      */ | ||||||
|     getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { |     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { | ||||||
|         return getProvider(id).getLearningObjectHTML(id); |         return getProvider(id).getLearningObjectHTML(id); | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ class AudioProcessor extends StringProcessor { | ||||||
|         super(DwengoContentType.AUDIO_MPEG); |         super(DwengoContentType.AUDIO_MPEG); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected renderFn(audioUrl: string): string { |     override renderFn(audioUrl: string): string { | ||||||
|         return DOMPurify.sanitize(`<audio controls>
 |         return DOMPurify.sanitize(`<audio controls>
 | ||||||
|             <source src="${audioUrl}" type=${type}> |             <source src="${audioUrl}" type=${type}> | ||||||
|             Your browser does not support the audio element. |             Your browser does not support the audio element. | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ class ExternProcessor extends StringProcessor { | ||||||
|         super(DwengoContentType.EXTERN); |         super(DwengoContentType.EXTERN); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override renderFn(externURL: string) { |     override renderFn(externURL: string): string { | ||||||
|         if (!isValidHttpUrl(externURL)) { |         if (!isValidHttpUrl(externURL)) { | ||||||
|             throw new ProcessingError('The url is not valid: ' + externURL); |             throw new ProcessingError('The url is not valid: ' + externURL); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ class GiftProcessor extends StringProcessor { | ||||||
|         super(DwengoContentType.GIFT); |         super(DwengoContentType.GIFT); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override renderFn(giftString: string) { |     override renderFn(giftString: string): string { | ||||||
|         const quizQuestions: GIFTQuestion[] = parse(giftString); |         const quizQuestions: GIFTQuestion[] = parse(giftString); | ||||||
| 
 | 
 | ||||||
|         let html = "<div class='learning-object-gift'>\n"; |         let html = "<div class='learning-object-gift'>\n"; | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { Category } from 'gift-pegjs'; | ||||||
| import { ProcessingError } from '../../processing-error.js'; | import { ProcessingError } from '../../processing-error.js'; | ||||||
| 
 | 
 | ||||||
| export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> { | export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> { | ||||||
|     render(question: Category, questionNumber: number): string { |     override render(_question: Category, _questionNumber: number): string { | ||||||
|         throw new ProcessingError("The question type 'Category' is not supported yet!"); |         throw new ProcessingError("The question type 'Category' is not supported yet!"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { Description } from 'gift-pegjs'; | ||||||
| import { ProcessingError } from '../../processing-error.js'; | import { ProcessingError } from '../../processing-error.js'; | ||||||
| 
 | 
 | ||||||
| export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> { | export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> { | ||||||
|     render(question: Description, questionNumber: number): string { |     override render(_question: Description, _questionNumber: number): string { | ||||||
|         throw new ProcessingError("The question type 'Description' is not supported yet!"); |         throw new ProcessingError("The question type 'Description' is not supported yet!"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import { GIFTQuestionRenderer } from './gift-question-renderer.js'; | ||||||
| import { Essay } from 'gift-pegjs'; | import { Essay } from 'gift-pegjs'; | ||||||
| 
 | 
 | ||||||
| export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> { | export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> { | ||||||
|     render(question: Essay, questionNumber: number): string { |     override render(question: Essay, questionNumber: number): string { | ||||||
|         let renderedHtml = ''; |         let renderedHtml = ''; | ||||||
|         if (question.title) { |         if (question.title) { | ||||||
|             renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`; |             renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`; | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { Matching } from 'gift-pegjs'; | ||||||
| import { ProcessingError } from '../../processing-error.js'; | import { ProcessingError } from '../../processing-error.js'; | ||||||
| 
 | 
 | ||||||
| export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> { | export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> { | ||||||
|     render(question: Matching, questionNumber: number): string { |     override render(_question: Matching, _questionNumber: number): string { | ||||||
|         throw new ProcessingError("The question type 'Matching' is not supported yet!"); |         throw new ProcessingError("The question type 'Matching' is not supported yet!"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import { GIFTQuestionRenderer } from './gift-question-renderer.js'; | ||||||
| import { MultipleChoice } from 'gift-pegjs'; | import { MultipleChoice } from 'gift-pegjs'; | ||||||
| 
 | 
 | ||||||
| export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> { | export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> { | ||||||
|     render(question: MultipleChoice, questionNumber: number): string { |     override render(question: MultipleChoice, questionNumber: number): string { | ||||||
|         let renderedHtml = ''; |         let renderedHtml = ''; | ||||||
|         if (question.title) { |         if (question.title) { | ||||||
|             renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`; |             renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`; | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { Numerical } from 'gift-pegjs'; | ||||||
| import { ProcessingError } from '../../processing-error.js'; | import { ProcessingError } from '../../processing-error.js'; | ||||||
| 
 | 
 | ||||||
| export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> { | export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> { | ||||||
|     render(question: Numerical, questionNumber: number): string { |     override render(_question: Numerical, _questionNumber: number): string { | ||||||
|         throw new ProcessingError("The question type 'Numerical' is not supported yet!"); |         throw new ProcessingError("The question type 'Numerical' is not supported yet!"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { ShortAnswer } from 'gift-pegjs'; | ||||||
| import { ProcessingError } from '../../processing-error.js'; | import { ProcessingError } from '../../processing-error.js'; | ||||||
| 
 | 
 | ||||||
| export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> { | export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> { | ||||||
|     render(question: ShortAnswer, questionNumber: number): string { |     override render(_question: ShortAnswer, _questionNumber: number): string { | ||||||
|         throw new ProcessingError("The question type 'ShortAnswer' is not supported yet!"); |         throw new ProcessingError("The question type 'ShortAnswer' is not supported yet!"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { TrueFalse } from 'gift-pegjs'; | ||||||
| import { ProcessingError } from '../../processing-error.js'; | import { ProcessingError } from '../../processing-error.js'; | ||||||
| 
 | 
 | ||||||
| export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> { | export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> { | ||||||
|     render(question: TrueFalse, questionNumber: number): string { |     override render(_question: TrueFalse, _questionNumber: number): string { | ||||||
|         throw new ProcessingError("The question type 'TrueFalse' is not supported yet!"); |         throw new ProcessingError("The question type 'TrueFalse' is not supported yet!"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ class BlockImageProcessor extends InlineImageProcessor { | ||||||
|         super(); |         super(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override renderFn(imageUrl: string) { |     override renderFn(imageUrl: string): string { | ||||||
|         const inlineHtml = super.render(imageUrl); |         const inlineHtml = super.render(imageUrl); | ||||||
|         return DOMPurify.sanitize(`<div>${inlineHtml}</div>`); |         return DOMPurify.sanitize(`<div>${inlineHtml}</div>`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ class InlineImageProcessor extends StringProcessor { | ||||||
|         super(contentType); |         super(contentType); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override renderFn(imageUrl: string) { |     override renderFn(imageUrl: string): string { | ||||||
|         if (!isValidHttpUrl(imageUrl)) { |         if (!isValidHttpUrl(imageUrl)) { | ||||||
|             throw new ProcessingError(`Image URL is invalid: ${imageUrl}`); |             throw new ProcessingError(`Image URL is invalid: ${imageUrl}`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -14,26 +14,24 @@ class MarkdownProcessor extends StringProcessor { | ||||||
|         super(DwengoContentType.TEXT_MARKDOWN); |         super(DwengoContentType.TEXT_MARKDOWN); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override renderFn(mdText: string) { |     static replaceLinks(html: string): string { | ||||||
|         let html = ''; |  | ||||||
|         try { |  | ||||||
|             marked.use({ renderer: dwengoMarkedRenderer }); |  | ||||||
|             html = marked(mdText, { async: false }); |  | ||||||
|             html = this.replaceLinks(html); // Replace html image links path
 |  | ||||||
|         } catch (e: any) { |  | ||||||
|             throw new ProcessingError(e.message); |  | ||||||
|         } |  | ||||||
|         return html; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     replaceLinks(html: string) { |  | ||||||
|         const proc = new InlineImageProcessor(); |         const proc = new InlineImageProcessor(); | ||||||
|         html = html.replace( |         html = html.replace( | ||||||
|             /<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g, |             /<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g, | ||||||
|             (match: string, src: string, alt: string, altText: string, title: string, titleText: string) => proc.render(src) |             (_match: string, src: string, _alt: string, _altText: string, _title: string, _titleText: string) => proc.render(src) | ||||||
|         ); |         ); | ||||||
|         return html; |         return html; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     override renderFn(mdText: string): string { | ||||||
|  |         try { | ||||||
|  |             marked.use({ renderer: dwengoMarkedRenderer }); | ||||||
|  |             const html = marked(mdText, { async: false }); | ||||||
|  |             return MarkdownProcessor.replaceLinks(html); // Replace html image links path
 | ||||||
|  |         } catch (e: unknown) { | ||||||
|  |             throw new ProcessingError('Unknown error while processing markdown: ' + e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export { MarkdownProcessor }; | export { MarkdownProcessor }; | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ class PdfProcessor extends StringProcessor { | ||||||
|         super(DwengoContentType.APPLICATION_PDF); |         super(DwengoContentType.APPLICATION_PDF); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override renderFn(pdfUrl: string) { |     override renderFn(pdfUrl: string): string { | ||||||
|         if (!isValidHttpUrl(pdfUrl)) { |         if (!isValidHttpUrl(pdfUrl)) { | ||||||
|             throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`); |             throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" l | ||||||
| const LEARNING_OBJECT_DOES_NOT_EXIST = "<div class='non-existing-learning-object' />"; | const LEARNING_OBJECT_DOES_NOT_EXIST = "<div class='non-existing-learning-object' />"; | ||||||
| 
 | 
 | ||||||
| class ProcessingService { | class ProcessingService { | ||||||
|     private processors!: Map<DwengoContentType, Processor<any>>; |     private processors!: Map<DwengoContentType, Processor<DwengoContentType>>; | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         const processors = [ |         const processors = [ | ||||||
|  |  | ||||||
|  | @ -9,7 +9,9 @@ import { DwengoContentType } from './content-type.js'; | ||||||
|  * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processor.js
 |  * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processor.js
 | ||||||
|  */ |  */ | ||||||
| abstract class Processor<T> { | abstract class Processor<T> { | ||||||
|     protected constructor(public contentType: DwengoContentType) {} |     protected constructor(public contentType: DwengoContentType) { | ||||||
|  |         // Do nothing
 | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Render the given object. |      * Render the given object. | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ class TextProcessor extends StringProcessor { | ||||||
|         super(DwengoContentType.TEXT_PLAIN); |         super(DwengoContentType.TEXT_PLAIN); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override renderFn(text: string) { |     override renderFn(text: string): string { | ||||||
|         // Sanitize plain text to prevent xss.
 |         // Sanitize plain text to prevent xss.
 | ||||||
|         return DOMPurify.sanitize(text); |         return DOMPurify.sanitize(text); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -18,14 +18,14 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma | ||||||
|     // Its corresponding learning object.
 |     // Its corresponding learning object.
 | ||||||
|     const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( |     const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( | ||||||
|         await Promise.all( |         await Promise.all( | ||||||
|             nodes.map((node) => |             nodes.map(async (node) => | ||||||
|                 learningObjectService |                 learningObjectService | ||||||
|                     .getLearningObjectById({ |                     .getLearningObjectById({ | ||||||
|                         hruid: node.learningObjectHruid, |                         hruid: node.learningObjectHruid, | ||||||
|                         version: node.version, |                         version: node.version, | ||||||
|                         language: node.language, |                         language: node.language, | ||||||
|                     }) |                     }) | ||||||
|                     .then((learningObject) => <[LearningPathNode, FilteredLearningObject | null]>[node, learningObject]) |                     .then((learningObject) => [node, learningObject] as [LearningPathNode, FilteredLearningObject | null]) | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|     ); |     ); | ||||||
|  | @ -117,7 +117,7 @@ async function convertNodes( | ||||||
|     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>, |     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>, | ||||||
|     personalizedFor?: PersonalizationTarget |     personalizedFor?: PersonalizationTarget | ||||||
| ): Promise<LearningObjectNode[]> { | ): Promise<LearningObjectNode[]> { | ||||||
|     const nodesPromise = Array.from(nodesToLearningObjects.entries()).map((entry) => |     const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => | ||||||
|         convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects) |         convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects) | ||||||
|     ); |     ); | ||||||
|     return await Promise.all(nodesPromise); |     return await Promise.all(nodesPromise); | ||||||
|  | @ -152,7 +152,7 @@ function convertTransition( | ||||||
|         throw new Error(`Learning object ${transition.next.learningObjectHruid}/${transition.next.language}/${transition.next.version} not found!`); |         throw new Error(`Learning object ${transition.next.learningObjectHruid}/${transition.next.language}/${transition.next.version} not found!`); | ||||||
|     } else { |     } else { | ||||||
|         return { |         return { | ||||||
|             _id: '' + index, // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
 |             _id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
 | ||||||
|             default: false, // We don't work with default transitions but retain this for backwards compatibility.
 |             default: false, // We don't work with default transitions but retain this for backwards compatibility.
 | ||||||
|             next: { |             next: { | ||||||
|                 _id: nextNode._id + index, // Construct a unique ID for the transition for backwards compatibility.
 |                 _id: nextNode._id + index, // Construct a unique ID for the transition for backwards compatibility.
 | ||||||
|  | @ -179,11 +179,11 @@ const databaseLearningPathProvider: LearningPathProvider = { | ||||||
|     ): Promise<LearningPathResponse> { |     ): Promise<LearningPathResponse> { | ||||||
|         const learningPathRepo = getLearningPathRepository(); |         const learningPathRepo = getLearningPathRepository(); | ||||||
| 
 | 
 | ||||||
|         const learningPaths = (await Promise.all(hruids.map((hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter( |         const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter( | ||||||
|             (learningPath) => learningPath !== null |             (learningPath) => learningPath !== null | ||||||
|         ); |         ); | ||||||
|         const filteredLearningPaths = await Promise.all( |         const filteredLearningPaths = await Promise.all( | ||||||
|             learningPaths.map((learningPath, index) => convertLearningPath(learningPath, index, personalizedFor)) |             learningPaths.map(async (learningPath, index) => convertLearningPath(learningPath, index, personalizedFor)) | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         return { |         return { | ||||||
|  | @ -200,7 +200,7 @@ const databaseLearningPathProvider: LearningPathProvider = { | ||||||
|         const learningPathRepo = getLearningPathRepository(); |         const learningPathRepo = getLearningPathRepository(); | ||||||
| 
 | 
 | ||||||
|         const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); |         const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); | ||||||
|         return await Promise.all(searchResults.map((result, index) => convertLearningPath(result, index, personalizedFor))); |         return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor))); | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js'; | import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js'; | ||||||
| import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; | import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; | ||||||
| import databaseLearningPathProvider from './database-learning-path-provider.js'; | import databaseLearningPathProvider from './database-learning-path-provider.js'; | ||||||
| import { EnvVars, getEnvVar } from '../../util/envvars.js'; | import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||||
| import { Language } from '../../entities/content/language.js'; | import { Language } from '../../entities/content/language.js'; | ||||||
| import { PersonalizationTarget } from './learning-path-personalization-util.js'; | import { PersonalizationTarget } from './learning-path-personalization-util.js'; | ||||||
| 
 | 
 | ||||||
| const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix); | const userContentPrefix = getEnvVar(envVars.UserContentPrefix); | ||||||
| const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; | const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -49,7 +49,9 @@ const learningPathService = { | ||||||
|      * Search learning paths in the data source using the given search string. |      * Search learning paths in the data source using the given search string. | ||||||
|      */ |      */ | ||||||
|     async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> { |     async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> { | ||||||
|         const providerResponses = await Promise.all(allProviders.map((provider) => provider.searchLearningPaths(query, language, personalizedFor))); |         const providerResponses = await Promise.all( | ||||||
|  |             allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor)) | ||||||
|  |         ); | ||||||
|         return providerResponses.flat(); |         return providerResponses.flat(); | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -2,11 +2,9 @@ import { getAnswerRepository, getQuestionRepository } from '../data/repositories | ||||||
| import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; | import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; | ||||||
| import { Question } from '../entities/questions/question.entity.js'; | import { Question } from '../entities/questions/question.entity.js'; | ||||||
| import { Answer } from '../entities/questions/answer.entity.js'; | import { Answer } from '../entities/questions/answer.entity.js'; | ||||||
| import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js'; | import { AnswerDTO, AnswerId, mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js'; | ||||||
| import { QuestionRepository } from '../data/questions/question-repository.js'; | import { QuestionRepository } from '../data/questions/question-repository.js'; | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
| import { mapToUser } from '../interfaces/user.js'; |  | ||||||
| import { Student } from '../entities/users/student.entity.js'; |  | ||||||
| import { mapToStudent } from '../interfaces/student.js'; | import { mapToStudent } from '../interfaces/student.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { | export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { | ||||||
|  | @ -47,7 +45,7 @@ export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO | | ||||||
|     return mapToQuestionDTO(question); |     return mapToQuestionDTO(question); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) { | export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> { | ||||||
|     const answerRepository = getAnswerRepository(); |     const answerRepository = getAnswerRepository(); | ||||||
|     const question = await fetchQuestion(questionId); |     const question = await fetchQuestion(questionId); | ||||||
| 
 | 
 | ||||||
|  | @ -70,7 +68,7 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean | ||||||
|     return answersDTO.map(mapToAnswerId); |     return answersDTO.map(mapToAnswerId); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createQuestion(questionDTO: QuestionDTO) { | export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> { | ||||||
|     const questionRepository = getQuestionRepository(); |     const questionRepository = getQuestionRepository(); | ||||||
| 
 | 
 | ||||||
|     const author = mapToStudent(questionDTO.author); |     const author = mapToStudent(questionDTO.author); | ||||||
|  | @ -81,14 +79,14 @@ export async function createQuestion(questionDTO: QuestionDTO) { | ||||||
|             author, |             author, | ||||||
|             content: questionDTO.content, |             content: questionDTO.content, | ||||||
|         }); |         }); | ||||||
|     } catch (e) { |     } catch (_) { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return questionDTO; |     return questionDTO; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteQuestion(questionId: QuestionId) { | export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO | null> { | ||||||
|     const questionRepository = getQuestionRepository(); |     const questionRepository = getQuestionRepository(); | ||||||
| 
 | 
 | ||||||
|     const question = await fetchQuestion(questionId); |     const question = await fetchQuestion(questionId); | ||||||
|  | @ -99,7 +97,7 @@ export async function deleteQuestion(questionId: QuestionId) { | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|         await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(questionId.learningObjectIdentifier, questionId.sequenceNumber); |         await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(questionId.learningObjectIdentifier, questionId.sequenceNumber); | ||||||
|     } catch (e) { |     } catch (_) { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,11 @@ | ||||||
| import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; | import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; | ||||||
| import { Class } from '../entities/classes/class.entity.js'; |  | ||||||
| import { Student } from '../entities/users/student.entity.js'; |  | ||||||
| import { AssignmentDTO } from '../interfaces/assignment.js'; | import { AssignmentDTO } from '../interfaces/assignment.js'; | ||||||
| import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | ||||||
| import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | ||||||
| import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; | import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; | ||||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | ||||||
| import { getAllAssignments } from './assignments.js'; | import { getAllAssignments } from './assignments.js'; | ||||||
|  | import { getLogger } from '../logging/initalize.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
|  | @ -28,15 +27,9 @@ export async function getStudent(username: string): Promise<StudentDTO | null> { | ||||||
| export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> { | export async function createStudent(userData: StudentDTO): Promise<StudentDTO | null> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
| 
 | 
 | ||||||
|     try { |     const newStudent = mapToStudent(userData); | ||||||
|         const newStudent = studentRepository.create(mapToStudent(userData)); |     await studentRepository.save(newStudent, { preventOverwrite: true }); | ||||||
|         await studentRepository.save(newStudent); |     return mapToStudentDTO(newStudent); | ||||||
| 
 |  | ||||||
|         return mapToStudentDTO(newStudent); |  | ||||||
|     } catch (e) { |  | ||||||
|         console.log(e); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteStudent(username: string): Promise<StudentDTO | null> { | export async function deleteStudent(username: string): Promise<StudentDTO | null> { | ||||||
|  | @ -53,7 +46,7 @@ export async function deleteStudent(username: string): Promise<StudentDTO | null | ||||||
| 
 | 
 | ||||||
|         return mapToStudentDTO(user); |         return mapToStudentDTO(user); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|         console.log(e); |         getLogger().error(e); | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -87,9 +80,7 @@ export async function getStudentAssignments(username: string, full: boolean): Pr | ||||||
|     const classRepository = getClassRepository(); |     const classRepository = getClassRepository(); | ||||||
|     const classes = await classRepository.findByStudent(student); |     const classes = await classRepository.findByStudent(student); | ||||||
| 
 | 
 | ||||||
|     const assignments = (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat(); |     return (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat(); | ||||||
| 
 |  | ||||||
|     return assignments; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> { | export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[]> { | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; | import { getSubmissionRepository } from '../data/repositories.js'; | ||||||
| import { Language } from '../entities/content/language.js'; | import { Language } from '../entities/content/language.js'; | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
| import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; | import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; | ||||||
|  | @ -21,21 +21,26 @@ export async function getSubmission( | ||||||
|     return mapToSubmissionDTO(submission); |     return mapToSubmissionDTO(submission); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createSubmission(submissionDTO: SubmissionDTO) { | export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO | null> { | ||||||
|     const submissionRepository = getSubmissionRepository(); |     const submissionRepository = getSubmissionRepository(); | ||||||
|     const submission = mapToSubmission(submissionDTO); |     const submission = mapToSubmission(submissionDTO); | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|         const newSubmission = await submissionRepository.create(submission); |         const newSubmission = submissionRepository.create(submission); | ||||||
|         await submissionRepository.save(newSubmission); |         await submissionRepository.save(newSubmission); | ||||||
|     } catch (e) { |     } catch (_) { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return mapToSubmissionDTO(submission); |     return mapToSubmissionDTO(submission); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) { | export async function deleteSubmission( | ||||||
|  |     learningObjectHruid: string, | ||||||
|  |     language: Language, | ||||||
|  |     version: number, | ||||||
|  |     submissionNumber: number | ||||||
|  | ): Promise<SubmissionDTO | null> { | ||||||
|     const submissionRepository = getSubmissionRepository(); |     const submissionRepository = getSubmissionRepository(); | ||||||
| 
 | 
 | ||||||
|     const submission = getSubmission(learningObjectHruid, language, version, submissionNumber); |     const submission = getSubmission(learningObjectHruid, language, version, submissionNumber); | ||||||
|  |  | ||||||
|  | @ -1,18 +1,10 @@ | ||||||
| import { | import { getClassRepository, getLearningObjectRepository, getQuestionRepository, getTeacherRepository } from '../data/repositories.js'; | ||||||
|     getClassRepository, |  | ||||||
|     getLearningObjectRepository, |  | ||||||
|     getQuestionRepository, |  | ||||||
|     getStudentRepository, |  | ||||||
|     getTeacherRepository, |  | ||||||
| } from '../data/repositories.js'; |  | ||||||
| import { Teacher } from '../entities/users/teacher.entity.js'; |  | ||||||
| import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | ||||||
| import { getClassStudents } from './classes.js'; | import { getClassStudents } from './classes.js'; | ||||||
| import { StudentDTO } from '../interfaces/student.js'; | import { StudentDTO } from '../interfaces/student.js'; | ||||||
| import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; | import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; | ||||||
| import { mapToUser } from '../interfaces/user.js'; |  | ||||||
| import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js'; | import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js'; | ||||||
| import { teachersOnly } from '../middleware/auth/auth.js'; | import { getLogger } from '../logging/initalize.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { | export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository = getTeacherRepository(); | ||||||
|  | @ -34,15 +26,10 @@ export async function getTeacher(username: string): Promise<TeacherDTO | null> { | ||||||
| export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> { | export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO | null> { | ||||||
|     const teacherRepository = getTeacherRepository(); |     const teacherRepository = getTeacherRepository(); | ||||||
| 
 | 
 | ||||||
|     try { |     const newTeacher = mapToTeacher(userData); | ||||||
|         const newTeacher = teacherRepository.create(mapToTeacher(userData)); |     await teacherRepository.save(newTeacher, { preventOverwrite: true }); | ||||||
|         await teacherRepository.save(newTeacher); |  | ||||||
| 
 | 
 | ||||||
|         return mapToTeacherDTO(newTeacher); |     return mapToTeacherDTO(newTeacher); | ||||||
|     } catch (e) { |  | ||||||
|         console.log(e); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteTeacher(username: string): Promise<TeacherDTO | null> { | export async function deleteTeacher(username: string): Promise<TeacherDTO | null> { | ||||||
|  | @ -59,7 +46,7 @@ export async function deleteTeacher(username: string): Promise<TeacherDTO | null | ||||||
| 
 | 
 | ||||||
|         return mapToTeacherDTO(user); |         return mapToTeacherDTO(user); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|         console.log(e); |         getLogger().error(e); | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ import { EntityProperty, EventArgs, EventSubscriber } from '@mikro-orm/core'; | ||||||
|  *   the sequence number will not be filled in. |  *   the sequence number will not be filled in. | ||||||
|  */ |  */ | ||||||
| export class SqliteAutoincrementSubscriber implements EventSubscriber { | export class SqliteAutoincrementSubscriber implements EventSubscriber { | ||||||
|     private sequenceNumbersForEntityType: Map<string, number> = new Map(); |     private sequenceNumbersForEntityType = new Map<string, number>(); | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * When an entity with an autoincremented property which is part of the composite private key is created, |      * When an entity with an autoincremented property which is part of the composite private key is created, | ||||||
|  | @ -27,14 +27,14 @@ export class SqliteAutoincrementSubscriber implements EventSubscriber { | ||||||
| 
 | 
 | ||||||
|         for (const prop of Object.values(args.meta.properties)) { |         for (const prop of Object.values(args.meta.properties)) { | ||||||
|             const property = prop as EntityProperty<T>; |             const property = prop as EntityProperty<T>; | ||||||
|             if (property.primary && property.autoincrement && !(args.entity as Record<string, any>)[property.name]) { |             if (property.primary && property.autoincrement && !(args.entity as Record<string, unknown>)[property.name]) { | ||||||
|                 // Obtain and increment sequence number of this entity.
 |                 // Obtain and increment sequence number of this entity.
 | ||||||
|                 const propertyKey = args.meta.class.name + '.' + property.name; |                 const propertyKey = args.meta.class.name + '.' + property.name; | ||||||
|                 const nextSeqNumber = this.sequenceNumbersForEntityType.get(propertyKey) || 0; |                 const nextSeqNumber = this.sequenceNumbersForEntityType.get(propertyKey) || 0; | ||||||
|                 this.sequenceNumbersForEntityType.set(propertyKey, nextSeqNumber + 1); |                 this.sequenceNumbersForEntityType.set(propertyKey, nextSeqNumber + 1); | ||||||
| 
 | 
 | ||||||
|                 // Set the property accordingly.
 |                 // Set the property accordingly.
 | ||||||
|                 (args.entity as Record<string, any>)[property.name] = nextSeqNumber + 1; |                 (args.entity as Record<string, unknown>)[property.name] = nextSeqNumber + 1; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import axios, { AxiosRequestConfig } from 'axios'; | import axios, { AxiosRequestConfig } from 'axios'; | ||||||
| import { getLogger, Logger } from '../logging/initalize.js'; | import { getLogger, Logger } from '../logging/initalize.js'; | ||||||
|  | import { LearningObjectIdentifier } from '../interfaces/learning-content.js'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|  | @ -17,8 +18,8 @@ export async function fetchWithLogging<T>( | ||||||
|     url: string, |     url: string, | ||||||
|     description: string, |     description: string, | ||||||
|     options?: { |     options?: { | ||||||
|         params?: Record<string, any>; |         params?: Record<string, unknown> | LearningObjectIdentifier; | ||||||
|         query?: Record<string, any>; |         query?: Record<string, unknown>; | ||||||
|         responseType?: 'json' | 'text'; |         responseType?: 'json' | 'text'; | ||||||
|     } |     } | ||||||
| ): Promise<T | null> { | ): Promise<T | null> { | ||||||
|  | @ -26,18 +27,21 @@ export async function fetchWithLogging<T>( | ||||||
|         const config: AxiosRequestConfig = options || {}; |         const config: AxiosRequestConfig = options || {}; | ||||||
|         const response = await axios.get<T>(url, config); |         const response = await axios.get<T>(url, config); | ||||||
|         return response.data; |         return response.data; | ||||||
|     } catch (error: any) { |     } catch (error: unknown) { | ||||||
|         if (error.response) { |         if (axios.isAxiosError(error)) { | ||||||
|             if (error.response.status === 404) { |             if (error.response) { | ||||||
|                 logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`); |                 if (error.response.status === 404) { | ||||||
|  |                     logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`); | ||||||
|  |                 } else { | ||||||
|  |                     logger.debug( | ||||||
|  |                         `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|             } else { |             } else { | ||||||
|                 logger.debug( |                 logger.debug(`❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message); | ||||||
|                     `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` |  | ||||||
|                 ); |  | ||||||
|             } |             } | ||||||
|         } else { |  | ||||||
|             logger.debug(`❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message); |  | ||||||
|         } |         } | ||||||
|  |         logger.error(`❌ ERROR: Unknown error while fetching ${description}.`, error); | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,7 +6,11 @@ | ||||||
|  * @param regex |  * @param regex | ||||||
|  * @param replacementFn |  * @param replacementFn | ||||||
|  */ |  */ | ||||||
| export async function replaceAsync(str: string, regex: RegExp, replacementFn: (match: string, ...args: string[]) => Promise<string>) { | export async function replaceAsync( | ||||||
|  |     str: string, | ||||||
|  |     regex: RegExp, | ||||||
|  |     replacementFn: (match: string, ...args: string[]) => Promise<string> | ||||||
|  | ): Promise<string> { | ||||||
|     const promises: Promise<string>[] = []; |     const promises: Promise<string>[] = []; | ||||||
| 
 | 
 | ||||||
|     // First run through matches: add all Promises resulting from the replacement function
 |     // First run through matches: add all Promises resulting from the replacement function
 | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger