Merge branch 'dev' into feat/frontend-controllers-adriaan
This commit is contained in:
		
						commit
						9f975977e0
					
				
					 257 changed files with 6698 additions and 3672 deletions
				
			
		
							
								
								
									
										21
									
								
								backend/.env.staging
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/.env.staging
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| PORT=3000 | ||||
| DWENGO_DB_HOST=db | ||||
| DWENGO_DB_PORT=5432 | ||||
| DWENGO_DB_USERNAME=postgres | ||||
| DWENGO_DB_PASSWORD=postgres | ||||
| DWENGO_DB_UPDATE=false | ||||
| 
 | ||||
| DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student | ||||
| DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo | ||||
| DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs | ||||
| DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher | ||||
| DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | ||||
| DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs | ||||
| 
 | ||||
| # Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production! | ||||
| #DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost | ||||
| DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080 | ||||
| 
 | ||||
| # Logging and monitoring | ||||
| 
 | ||||
| LOKI_HOST=http://logging:3102 | ||||
|  | @ -1,3 +1,13 @@ | |||
| PORT=3000 | ||||
| DWENGO_DB_UPDATE=true | ||||
| # | ||||
| # Test environment configuration | ||||
| # | ||||
| # Should not need to be modified. | ||||
| # See .env.example for more information. | ||||
| # | ||||
| 
 | ||||
| ### Dwengo ### | ||||
| 
 | ||||
| DWENGO_PORT=3000 | ||||
| 
 | ||||
| DWENGO_DB_NAME=":memory:" | ||||
| DWENGO_DB_UPDATE=true | ||||
|  |  | |||
|  | @ -1,13 +0,0 @@ | |||
| # | ||||
| # Test environment configuration | ||||
| # | ||||
| # Should not need to be modified. | ||||
| # See .env.example for more information. | ||||
| # | ||||
| 
 | ||||
| ### Dwengo ### | ||||
| 
 | ||||
| DWENGO_PORT=3000 | ||||
| 
 | ||||
| DWENGO_DB_NAME=":memory:" | ||||
| DWENGO_DB_UPDATE=true | ||||
|  | @ -1,38 +1,51 @@ | |||
| FROM node:22 AS build-stage | ||||
| 
 | ||||
| WORKDIR /app | ||||
| WORKDIR /app/dwengo | ||||
| 
 | ||||
| # Install dependencies | ||||
| 
 | ||||
| COPY package*.json ./ | ||||
| COPY backend/package.json ./backend/ | ||||
| # Backend depends on common | ||||
| COPY common/package.json ./common/ | ||||
| 
 | ||||
| RUN npm install --silent | ||||
| 
 | ||||
| # Build the backend | ||||
| 
 | ||||
| # Root tsconfig.json | ||||
| COPY tsconfig.json ./ | ||||
| COPY tsconfig.json tsconfig.build.json ./ | ||||
| 
 | ||||
| WORKDIR /app/backend | ||||
| 
 | ||||
| COPY backend ./ | ||||
| COPY docs /app/docs | ||||
| COPY backend ./backend | ||||
| COPY common ./common | ||||
| COPY docs ./docs | ||||
| 
 | ||||
| RUN npm run build | ||||
| 
 | ||||
| FROM node:22 AS production-stage | ||||
| 
 | ||||
| WORKDIR /app | ||||
| WORKDIR /app/dwengo | ||||
| 
 | ||||
| COPY package-lock.json backend/package.json ./ | ||||
| # Copy static files | ||||
| 
 | ||||
| COPY ./backend/i18n ./i18n | ||||
| 
 | ||||
| # Copy built files | ||||
| 
 | ||||
| COPY --from=build-stage /app/dwengo/common/dist ./common/dist | ||||
| COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist | ||||
| 
 | ||||
| COPY package*.json ./ | ||||
| COPY backend/package.json ./backend/ | ||||
| # Backend depends on common | ||||
| COPY common/package.json ./common/ | ||||
| 
 | ||||
| RUN npm install --silent --only=production | ||||
| 
 | ||||
| COPY ./docs /docs | ||||
| COPY ./backend/i18n /app/i18n | ||||
| COPY --from=build-stage /app/backend/dist ./dist/ | ||||
| COPY ./docs ./docs | ||||
| COPY ./backend/i18n ./backend/i18n | ||||
| COPY ./backend/.env ./backend/.env | ||||
| 
 | ||||
| EXPOSE 3000 | ||||
| 
 | ||||
| CMD ["node", "--env-file=.env", "dist/app.js"] | ||||
| CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"] | ||||
|  |  | |||
|  | @ -34,7 +34,9 @@ npm run test:unit | |||
| 
 | ||||
| ```shell | ||||
| # Omgevingsvariabelen | ||||
| cp .env.development.example .env | ||||
| cp .env.example .env | ||||
| # Configureer de .env file met de juiste waarden! | ||||
| nano .env | ||||
| 
 | ||||
| npm run build | ||||
| npm run start | ||||
|  |  | |||
|  | @ -1,7 +0,0 @@ | |||
| // Can be placed in dotenv but found it redundant
 | ||||
| // Import dotenv from "dotenv";
 | ||||
| // Load .env file
 | ||||
| // Dotenv.config();
 | ||||
| export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; | ||||
| export const FALLBACK_LANG = 'nl'; | ||||
| export const FALLBACK_SEQ_NUM = 1; | ||||
|  | @ -8,14 +8,4 @@ export default [ | |||
|             globals: globals.node, | ||||
|         }, | ||||
|     }, | ||||
| 
 | ||||
|     { | ||||
|         files: ['tests/**/*.ts'], | ||||
|         languageOptions: { | ||||
|             globals: globals.node, | ||||
|         }, | ||||
|         rules: { | ||||
|             'no-console': 'off', | ||||
|         }, | ||||
|     }, | ||||
| ]; | ||||
|  |  | |||
|  | @ -1,17 +1,19 @@ | |||
| { | ||||
|     "name": "dwengo-1-backend", | ||||
|     "name": "@dwengo-1/backend", | ||||
|     "version": "0.1.1", | ||||
|     "description": "Backend for Dwengo-1", | ||||
|     "private": true, | ||||
|     "type": "module", | ||||
|     "main": "dist/app.js", | ||||
|     "scripts": { | ||||
|         "build": "cross-env NODE_ENV=production tsc --project tsconfig.json", | ||||
|         "build": "cross-env NODE_ENV=production tsc --build", | ||||
|         "dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts", | ||||
|         "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", | ||||
|         "format": "prettier --write src/", | ||||
|         "format-check": "prettier --check src/", | ||||
|         "lint": "eslint . --fix", | ||||
|         "test:unit": "vitest" | ||||
|         "pretest:unit": "npm run build", | ||||
|         "test:unit": "vitest --run" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@mikro-orm/core": "6.4.9", | ||||
|  | @ -24,6 +26,7 @@ | |||
|         "cross": "^1.0.0", | ||||
|         "cross-env": "^7.0.3", | ||||
|         "dotenv": "^16.4.7", | ||||
|         "dwengo-1-common": "^0.1.1", | ||||
|         "express": "^5.0.1", | ||||
|         "express-jwt": "^8.5.1", | ||||
|         "gift-pegjs": "^1.0.2", | ||||
|  |  | |||
|  | @ -5,15 +5,16 @@ import cors from './middleware/cors.js'; | |||
| import { getLogger, Logger } from './logging/initalize.js'; | ||||
| import { responseTimeLogger } from './logging/responseTimeLogger.js'; | ||||
| import responseTime from 'response-time'; | ||||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||
| import { envVars, getNumericEnvVar } from './util/envVars.js'; | ||||
| import apiRouter from './routes/router.js'; | ||||
| import swaggerMiddleware from './swagger.js'; | ||||
| import swaggerUi from 'swagger-ui-express'; | ||||
| import { errorHandler } from './middleware/error-handling/error-handler.js'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| const app: Express = express(); | ||||
| const port: string | number = getNumericEnvVar(EnvVars.Port); | ||||
| const port: string | number = getNumericEnvVar(envVars.Port); | ||||
| 
 | ||||
| app.use(express.json()); | ||||
| app.use(cors); | ||||
|  | @ -26,7 +27,9 @@ app.use('/api', apiRouter); | |||
| // Swagger
 | ||||
| app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); | ||||
| 
 | ||||
| async function startServer() { | ||||
| app.use(errorHandler); | ||||
| 
 | ||||
| async function startServer(): Promise<void> { | ||||
|     await initORM(); | ||||
| 
 | ||||
|     app.listen(port, () => { | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { EnvVars, getEnvVar } from './util/envvars.js'; | ||||
| import { envVars, getEnvVar } from './util/envVars.js'; | ||||
| 
 | ||||
| // API
 | ||||
| export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); | ||||
| export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); | ||||
| export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl); | ||||
| export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage); | ||||
| 
 | ||||
| export const FALLBACK_SEQ_NUM = 1; | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; | ||||
| import { AssignmentDTO } from '../interfaces/assignment.js'; | ||||
| import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | ||||
| 
 | ||||
| // Typescript is annoy with with parameter forwarding from class.ts
 | ||||
| // Typescript is annoying with parameter forwarding from class.ts
 | ||||
| interface AssignmentParams { | ||||
|     classid: string; | ||||
|     id: string; | ||||
|  | @ -41,7 +41,7 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re | |||
| } | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
|     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> { | ||||
|     const classid = req.params.classid; | ||||
|     const assignmentNumber = +req.params.id; | ||||
|     const assignmentNumber = Number(req.params.id); | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     if (isNaN(assignmentNumber)) { | ||||
|  |  | |||
|  | @ -1,16 +1,16 @@ | |||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||
| import { envVars, getEnvVar } from '../util/envVars.js'; | ||||
| 
 | ||||
| type FrontendIdpConfig = { | ||||
| interface FrontendIdpConfig { | ||||
|     authority: string; | ||||
|     clientId: string; | ||||
|     scope: string; | ||||
|     responseType: string; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type FrontendAuthConfig = { | ||||
| interface FrontendAuthConfig { | ||||
|     student: FrontendIdpConfig; | ||||
|     teacher: FrontendIdpConfig; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| const SCOPE = 'openid profile email'; | ||||
| const RESPONSE_TYPE = 'code'; | ||||
|  | @ -18,14 +18,14 @@ const RESPONSE_TYPE = 'code'; | |||
| export function getFrontendAuthConfig(): FrontendAuthConfig { | ||||
|     return { | ||||
|         student: { | ||||
|             authority: getEnvVar(EnvVars.IdpStudentUrl), | ||||
|             clientId: getEnvVar(EnvVars.IdpStudentClientId), | ||||
|             authority: getEnvVar(envVars.IdpStudentUrl), | ||||
|             clientId: getEnvVar(envVars.IdpStudentClientId), | ||||
|             scope: SCOPE, | ||||
|             responseType: RESPONSE_TYPE, | ||||
|         }, | ||||
|         teacher: { | ||||
|             authority: getEnvVar(EnvVars.IdpTeacherUrl), | ||||
|             clientId: getEnvVar(EnvVars.IdpTeacherClientId), | ||||
|             authority: getEnvVar(envVars.IdpTeacherUrl), | ||||
|             clientId: getEnvVar(envVars.IdpTeacherClientId), | ||||
|             scope: SCOPE, | ||||
|             responseType: RESPONSE_TYPE, | ||||
|         }, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js'; | ||||
| import { ClassDTO } from '../interfaces/class.js'; | ||||
| import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | ||||
| 
 | ||||
| export async function getAllClassesHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
|  |  | |||
							
								
								
									
										18
									
								
								backend/src/controllers/error-helper.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend/src/controllers/error-helper.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Checks for the presence of required fields and throws a BadRequestException | ||||
|  * if any are missing. | ||||
|  * | ||||
|  * @param fields - An object with key-value pairs to validate. | ||||
|  */ | ||||
| export function requireFields(fields: Record<string, unknown>): void { | ||||
|     const missing = Object.entries(fields) | ||||
|         .filter(([_, value]) => value === undefined || value === null || value === '') | ||||
|         .map(([key]) => key); | ||||
| 
 | ||||
|     if (missing.length > 0) { | ||||
|         const message = `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`; | ||||
|         throw new BadRequestException(message); | ||||
|     } | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; | ||||
| import { GroupDTO } from '../interfaces/group.js'; | ||||
| import { GroupDTO } from '@dwengo-1/common/interfaces/group'; | ||||
| 
 | ||||
| // Typescript is annoywith with parameter forwarding from class.ts
 | ||||
| interface GroupParams { | ||||
|  | @ -12,14 +12,14 @@ interface GroupParams { | |||
| export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> { | ||||
|     const classId = req.params.classid; | ||||
|     const full = req.query.full === 'true'; | ||||
|     const assignmentId = +req.params.assignmentid; | ||||
|     const assignmentId = Number(req.params.assignmentid); | ||||
| 
 | ||||
|     if (isNaN(assignmentId)) { | ||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const groupId = +req.params.groupid!; // Can't be undefined
 | ||||
|     const groupId = Number(req.params.groupid!); // Can't be undefined
 | ||||
| 
 | ||||
|     if (isNaN(groupId)) { | ||||
|         res.status(400).json({ error: 'Group id must be a number' }); | ||||
|  | @ -40,7 +40,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< | |||
|     const classId = req.params.classid; | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     const assignmentId = +req.params.assignmentid; | ||||
|     const assignmentId = Number(req.params.assignmentid); | ||||
| 
 | ||||
|     if (isNaN(assignmentId)) { | ||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||
|  | @ -56,7 +56,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< | |||
| 
 | ||||
| export async function createGroupHandler(req: Request, res: Response): Promise<void> { | ||||
|     const classid = req.params.classid; | ||||
|     const assignmentId = +req.params.assignmentid; | ||||
|     const assignmentId = Number(req.params.assignmentid); | ||||
| 
 | ||||
|     if (isNaN(assignmentId)) { | ||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||
|  | @ -78,14 +78,14 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P | |||
|     const classId = req.params.classid; | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     const assignmentId = +req.params.assignmentid; | ||||
|     const assignmentId = Number(req.params.assignmentid); | ||||
| 
 | ||||
|     if (isNaN(assignmentId)) { | ||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const groupId = +req.params.groupid!; // Can't be undefined
 | ||||
|     const groupId = Number(req.params.groupid); // Can't be undefined
 | ||||
| 
 | ||||
|     if (isNaN(groupId)) { | ||||
|         res.status(400).json({ error: 'Group id must be a number' }); | ||||
|  |  | |||
|  | @ -1,20 +1,20 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js'; | ||||
| import learningObjectService from '../services/learning-objects/learning-object-service.js'; | ||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||
| import { Language } from '../entities/content/language.js'; | ||||
| import { BadRequestException } from '../exceptions.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import attachmentService from '../services/learning-objects/attachment-service.js'; | ||||
| import { NotFoundError } from '@mikro-orm/core'; | ||||
| import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||
| import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||
| import { envVars, getEnvVar } from '../util/envVars.js'; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| 
 | ||||
| function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | ||||
|     if (!req.params.hruid) { | ||||
|         throw new BadRequestException('HRUID is required.'); | ||||
|     } | ||||
|     return { | ||||
|         hruid: req.params.hruid as string, | ||||
|         language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, | ||||
|         hruid: req.params.hruid, | ||||
|         language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language, | ||||
|         version: parseInt(req.query.version as string), | ||||
|     }; | ||||
| } | ||||
|  | @ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif | |||
|         throw new BadRequestException('HRUID is required.'); | ||||
|     } | ||||
|     return { | ||||
|         hruid: req.params.hruid as string, | ||||
|         hruid: req.params.hruid, | ||||
|         language: (req.query.language as Language) || FALLBACK_LANG, | ||||
|     }; | ||||
| } | ||||
|  | @ -47,6 +47,11 @@ export async function getLearningObject(req: Request, res: Response): Promise<vo | |||
|     const learningObjectId = getLearningObjectIdentifierFromRequest(req); | ||||
| 
 | ||||
|     const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); | ||||
| 
 | ||||
|     if (!learningObject) { | ||||
|         throw new NotFoundException('Learning object not found'); | ||||
|     } | ||||
| 
 | ||||
|     res.json(learningObject); | ||||
| } | ||||
| 
 | ||||
|  | @ -63,7 +68,7 @@ export async function getAttachment(req: Request, res: Response): Promise<void> | |||
|     const attachment = await attachmentService.getAttachment(learningObjectId, name); | ||||
| 
 | ||||
|     if (!attachment) { | ||||
|         throw new NotFoundError(`Attachment ${name} not found`); | ||||
|         throw new NotFoundException(`Attachment ${name} not found`); | ||||
|     } | ||||
|     res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); | ||||
| } | ||||
|  |  | |||
|  | @ -2,13 +2,14 @@ import { Request, Response } from 'express'; | |||
| import { themes } from '../data/themes.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import learningPathService from '../services/learning-paths/learning-path-service.js'; | ||||
| import { BadRequestException, NotFoundException } from '../exceptions.js'; | ||||
| import { Language } from '../entities/content/language.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { | ||||
|     PersonalizationTarget, | ||||
|     personalizedForGroup, | ||||
|     personalizedForStudent, | ||||
| } from '../services/learning-paths/learning-path-personalization-util.js'; | ||||
| import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||
| import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Fetch learning paths based on query parameters. | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; | ||||
| import { QuestionDTO, QuestionId } from '../interfaces/question.js'; | ||||
| import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; | ||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||
| import { Language } from '../entities/content/language.js'; | ||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { | ||||
|     const { hruid, version } = req.params; | ||||
|  | @ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu | |||
|     return { | ||||
|         hruid, | ||||
|         language: (lang as Language) || FALLBACK_LANG, | ||||
|         version: +version, | ||||
|         version: Number(version), | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,101 +1,67 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { | ||||
|     createClassJoinRequest, | ||||
|     createStudent, | ||||
|     deleteClassJoinRequest, | ||||
|     deleteStudent, | ||||
|     getAllStudents, | ||||
|     getJoinRequestByStudentClass, | ||||
|     getJoinRequestsByStudent, | ||||
|     getStudent, | ||||
|     getStudentAssignments, | ||||
|     getStudentClasses, | ||||
|     getStudentGroups, | ||||
|     getStudentQuestions, | ||||
|     getStudentSubmissions, | ||||
| } from '../services/students.js'; | ||||
| import { StudentDTO } from '../interfaces/student.js'; | ||||
| import { requireFields } from './error-helper.js'; | ||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||
| 
 | ||||
| // TODO: accept arguments (full, ...)
 | ||||
| // TODO: endpoints
 | ||||
| export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     const students = await getAllStudents(full); | ||||
|     const students: StudentDTO[] | string[] = await getAllStudents(full); | ||||
| 
 | ||||
|     if (!students) { | ||||
|         res.status(404).json({ error: `Student not found.` }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ students: students }); | ||||
|     res.json({ students }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     const student = await getStudent(username); | ||||
| 
 | ||||
|     const user = await getStudent(username); | ||||
| 
 | ||||
|     if (!user) { | ||||
|         res.status(404).json({ | ||||
|             error: `User with username '${username}' not found.`, | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json(user); | ||||
|     res.json({ student }); | ||||
| } | ||||
| 
 | ||||
| export async function createStudentHandler(req: Request, res: Response) { | ||||
| export async function createStudentHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.body.username; | ||||
|     const firstName = req.body.firstName; | ||||
|     const lastName = req.body.lastName; | ||||
|     requireFields({ username, firstName, lastName }); | ||||
| 
 | ||||
|     const userData = req.body as StudentDTO; | ||||
| 
 | ||||
|     if (!userData.username || !userData.firstName || !userData.lastName) { | ||||
|         res.status(400).json({ | ||||
|             error: 'Missing required fields: username, firstName, lastName', | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const newUser = await createStudent(userData); | ||||
| 
 | ||||
|     if (!newUser) { | ||||
|         res.status(500).json({ | ||||
|             error: 'Something went wrong while creating student', | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(201).json(newUser); | ||||
|     const student = await createStudent(userData); | ||||
|     res.json({ student }); | ||||
| } | ||||
| 
 | ||||
| export async function deleteStudentHandler(req: Request, res: Response) { | ||||
| export async function deleteStudentHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const deletedUser = await deleteStudent(username); | ||||
|     if (!deletedUser) { | ||||
|         res.status(404).json({ | ||||
|             error: `User with username '${username}' not found.`, | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(200).json(deletedUser); | ||||
|     const student = await deleteStudent(username); | ||||
|     res.json({ student }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
|     const username = req.params.id; | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const classes = await getStudentClasses(username, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         classes: classes, | ||||
|     }); | ||||
|     res.json({ classes }); | ||||
| } | ||||
| 
 | ||||
| // TODO
 | ||||
|  | @ -104,33 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro | |||
| // Have this assignment.
 | ||||
| export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
|     const username = req.params.id; | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const assignments = getStudentAssignments(username, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         assignments: assignments, | ||||
|     }); | ||||
|     res.json({ assignments }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
|     const username = req.params.id; | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const groups = await getStudentGroups(username, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         groups: groups, | ||||
|     }); | ||||
|     res.json({ groups }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.id; | ||||
|     const username = req.params.username; | ||||
|     const full = req.query.full === 'true'; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const submissions = await getStudentSubmissions(username, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         submissions: submissions, | ||||
|     }); | ||||
|     res.json({ submissions }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentQuestionsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const questions = await getStudentQuestions(username, full); | ||||
| 
 | ||||
|     res.json({ questions }); | ||||
| } | ||||
| 
 | ||||
| export async function createStudentRequestHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     const classId = req.body.classId; | ||||
|     requireFields({ username, classId }); | ||||
| 
 | ||||
|     const request = await createClassJoinRequest(username, classId); | ||||
|     res.json({ request }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentRequestsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const requests = await getJoinRequestsByStudent(username); | ||||
|     res.json({ requests }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentRequestHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     const classId = req.params.classId; | ||||
|     requireFields({ username, classId }); | ||||
| 
 | ||||
|     const request = await getJoinRequestByStudentClass(username, classId); | ||||
|     res.json({ request }); | ||||
| } | ||||
| 
 | ||||
| export async function deleteClassJoinRequestHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     const classId = req.params.classId; | ||||
|     requireFields({ username, classId }); | ||||
| 
 | ||||
|     const request = await deleteClassJoinRequest(username, classId); | ||||
|     res.json({ request }); | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; | ||||
| import { Language, languageMap } from '../entities/content/language.js'; | ||||
| import { SubmissionDTO } from '../interfaces/submission'; | ||||
| import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; | ||||
| import { Language, languageMap } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| interface SubmissionParams { | ||||
|     hruid: string; | ||||
|  | @ -10,7 +10,7 @@ interface SubmissionParams { | |||
| 
 | ||||
| export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> { | ||||
|     const lohruid = req.params.hruid; | ||||
|     const submissionNumber = +req.params.id; | ||||
|     const submissionNumber = Number(req.params.id); | ||||
| 
 | ||||
|     if (isNaN(submissionNumber)) { | ||||
|         res.status(400).json({ error: 'Submission number is not a number' }); | ||||
|  | @ -30,7 +30,7 @@ export async function getSubmissionHandler(req: Request<SubmissionParams>, res: | |||
|     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 submission = await createSubmission(submissionDTO); | ||||
|  | @ -43,9 +43,9 @@ export async function createSubmissionHandler(req: Request, res: Response) { | |||
|     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 submissionNumber = +req.params.id; | ||||
|     const submissionNumber = Number(req.params.id); | ||||
| 
 | ||||
|     const lang = languageMap[req.query.language as string] || Language.Dutch; | ||||
|     const version = (req.query.version || 1) as number; | ||||
|  |  | |||
|  | @ -4,137 +4,97 @@ import { | |||
|     deleteTeacher, | ||||
|     getAllTeachers, | ||||
|     getClassesByTeacher, | ||||
|     getQuestionsByTeacher, | ||||
|     getJoinRequestsByClass, | ||||
|     getStudentsByTeacher, | ||||
|     getTeacher, | ||||
|     getTeacherQuestions, | ||||
|     updateClassJoinRequestStatus, | ||||
| } from '../services/teachers.js'; | ||||
| import { TeacherDTO } from '../interfaces/teacher.js'; | ||||
| import { requireFields } from './error-helper.js'; | ||||
| import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||
| 
 | ||||
| export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> { | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     const teachers = await getAllTeachers(full); | ||||
|     const teachers: TeacherDTO[] | string[] = await getAllTeachers(full); | ||||
| 
 | ||||
|     if (!teachers) { | ||||
|         res.status(404).json({ error: `Teacher not found.` }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ teachers: teachers }); | ||||
|     res.json({ teachers }); | ||||
| } | ||||
| 
 | ||||
| export async function getTeacherHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     const teacher = await getTeacher(username); | ||||
| 
 | ||||
|     const user = await getTeacher(username); | ||||
| 
 | ||||
|     if (!user) { | ||||
|         res.status(404).json({ | ||||
|             error: `Teacher '${username}' not found.`, | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json(user); | ||||
|     res.json({ teacher }); | ||||
| } | ||||
| 
 | ||||
| export async function createTeacherHandler(req: Request, res: Response) { | ||||
| export async function createTeacherHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.body.username; | ||||
|     const firstName = req.body.firstName; | ||||
|     const lastName = req.body.lastName; | ||||
|     requireFields({ username, firstName, lastName }); | ||||
| 
 | ||||
|     const userData = req.body as TeacherDTO; | ||||
| 
 | ||||
|     if (!userData.username || !userData.firstName || !userData.lastName) { | ||||
|         res.status(400).json({ | ||||
|             error: 'Missing required fields: username, firstName, lastName', | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const newUser = await createTeacher(userData); | ||||
| 
 | ||||
|     if (!newUser) { | ||||
|         res.status(400).json({ error: 'Failed to create teacher' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(201).json(newUser); | ||||
|     const teacher = await createTeacher(userData); | ||||
|     res.json({ teacher }); | ||||
| } | ||||
| 
 | ||||
| export async function deleteTeacherHandler(req: Request, res: Response) { | ||||
| export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const deletedUser = await deleteTeacher(username); | ||||
|     if (!deletedUser) { | ||||
|         res.status(404).json({ | ||||
|             error: `User '${username}' not found.`, | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(200).json(deletedUser); | ||||
|     const teacher = await deleteTeacher(username); | ||||
|     res.json({ teacher }); | ||||
| } | ||||
| 
 | ||||
| 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'; | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const classes = await getClassesByTeacher(username, full); | ||||
| 
 | ||||
|     if (!classes) { | ||||
|         res.status(404).json({ error: 'Teacher not found' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ classes: classes }); | ||||
|     res.json({ classes }); | ||||
| } | ||||
| 
 | ||||
| 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'; | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const students = await getStudentsByTeacher(username, full); | ||||
| 
 | ||||
|     if (!students) { | ||||
|         res.status(404).json({ error: 'Teacher not found' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ students: students }); | ||||
|     res.json({ students }); | ||||
| } | ||||
| 
 | ||||
| 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'; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     if (!username) { | ||||
|         res.status(400).json({ error: 'Missing required field: username' }); | ||||
|         return; | ||||
|     } | ||||
|     const questions = await getTeacherQuestions(username, full); | ||||
| 
 | ||||
|     const questions = await getQuestionsByTeacher(username, full); | ||||
| 
 | ||||
|     if (!questions) { | ||||
|         res.status(404).json({ error: 'Teacher not found' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json({ questions: questions }); | ||||
|     res.json({ questions }); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.query.username as string; | ||||
|     const classId = req.params.classId; | ||||
|     requireFields({ username, classId }); | ||||
| 
 | ||||
|     const joinRequests = await getJoinRequestsByClass(classId); | ||||
|     res.json({ joinRequests }); | ||||
| } | ||||
| 
 | ||||
| export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise<void> { | ||||
|     const studentUsername = req.query.studentUsername as string; | ||||
|     const classId = req.params.classId; | ||||
|     const accepted = req.body.accepted !== 'false'; // Default = true
 | ||||
|     requireFields({ studentUsername, classId }); | ||||
| 
 | ||||
|     const request = await updateClassJoinRequestStatus(studentUsername, classId, accepted); | ||||
|     res.json({ request }); | ||||
| } | ||||
|  |  | |||
|  | @ -3,25 +3,23 @@ import { themes } from '../data/themes.js'; | |||
| import { loadTranslations } from '../util/translation-helper.js'; | ||||
| 
 | ||||
| interface Translations { | ||||
|     curricula_page: { | ||||
|         [key: string]: { title: string; description?: string }; | ||||
|     }; | ||||
|     curricula_page: Record<string, { title: string; description?: string }>; | ||||
| } | ||||
| 
 | ||||
| export function getThemesHandler(req: Request, res: Response) { | ||||
|     const language = (req.query.language as string)?.toLowerCase() || 'nl'; | ||||
| export function getThemesHandler(req: Request, res: Response): void { | ||||
|     const language = ((req.query.language as string) || 'nl').toLowerCase(); | ||||
|     const translations = loadTranslations<Translations>(language); | ||||
|     const themeList = themes.map((theme) => ({ | ||||
|         key: theme.title, | ||||
|         title: translations.curricula_page[theme.title]?.title || theme.title, | ||||
|         description: translations.curricula_page[theme.title]?.description, | ||||
|         title: translations.curricula_page[theme.title].title || theme.title, | ||||
|         description: translations.curricula_page[theme.title].description, | ||||
|         image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, | ||||
|     })); | ||||
| 
 | ||||
|     res.json(themeList); | ||||
| } | ||||
| 
 | ||||
| export function getHruidsByThemeHandler(req: Request, res: Response) { | ||||
| export function getHruidsByThemeHandler(req: Request, res: Response): void { | ||||
|     const themeKey = req.params.theme; | ||||
| 
 | ||||
|     if (!themeKey) { | ||||
|  |  | |||
|  | @ -3,13 +3,13 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; | |||
| import { Class } from '../../entities/classes/class.entity.js'; | ||||
| 
 | ||||
| export class AssignmentRepository extends DwengoEntityRepository<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 }); | ||||
|     } | ||||
|     public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { | ||||
|     public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { | ||||
|         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 }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; | |||
| import { Student } from '../../entities/users/student.entity.js'; | ||||
| 
 | ||||
| export class GroupRepository extends DwengoEntityRepository<Group> { | ||||
|     public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { | ||||
|     public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { | ||||
|         return this.findOne( | ||||
|             { | ||||
|                 assignment: assignment, | ||||
|  | @ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> { | |||
|             { populate: ['members'] } | ||||
|         ); | ||||
|     } | ||||
|     public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { | ||||
|     public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { | ||||
|         return this.findAll({ | ||||
|             where: { assignment: assignment }, | ||||
|             populate: ['members'], | ||||
|         }); | ||||
|     } | ||||
|     public findAllGroupsWithStudent(student: Student): Promise<Group[]> { | ||||
|     public async findAllGroupsWithStudent(student: Student): Promise<Group[]> { | ||||
|         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({ | ||||
|             assignment: assignment, | ||||
|             groupNumber: groupNumber, | ||||
|  |  | |||
|  | @ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object | |||
| import { Student } from '../../entities/users/student.entity.js'; | ||||
| 
 | ||||
| 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({ | ||||
|             learningObjectHruid: loId.hruid, | ||||
|             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( | ||||
|             { | ||||
|                 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( | ||||
|             { | ||||
|                 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 }); | ||||
|     } | ||||
| 
 | ||||
|     public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { | ||||
|     public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { | ||||
|         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({ | ||||
|             learningObjectHruid: loId.hruid, | ||||
|             learningObjectLanguage: loId.language, | ||||
|  |  | |||
|  | @ -2,15 +2,19 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | |||
| import { Class } from '../../entities/classes/class.entity.js'; | ||||
| import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; | ||||
| import { Student } from '../../entities/users/student.entity.js'; | ||||
| import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; | ||||
| 
 | ||||
| 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 } }); | ||||
|     } | ||||
|     public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { | ||||
|         return this.findAll({ where: { class: clazz } }); | ||||
|     public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { | ||||
|         return this.findAll({ where: { class: clazz, status: ClassJoinRequestStatus.Open } }); // TODO check if works like this
 | ||||
|     } | ||||
|     public deleteBy(requester: Student, clazz: Class): Promise<void> { | ||||
|     public async findByStudentAndClass(requester: Student, clazz: Class): Promise<ClassJoinRequest | null> { | ||||
|         return this.findOne({ requester, class: clazz }); | ||||
|     } | ||||
|     public async deleteBy(requester: Student, clazz: Class): Promise<void> { | ||||
|         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'; | ||||
| 
 | ||||
| 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'] }); | ||||
|     } | ||||
|     public deleteById(id: string): Promise<void> { | ||||
|     public async deleteById(id: string): Promise<void> { | ||||
|         return this.deleteWhere({ classId: id }); | ||||
|     } | ||||
|     public findByStudent(student: Student): Promise<Class[]> { | ||||
|     public async findByStudent(student: Student): Promise<Class[]> { | ||||
|         return this.find( | ||||
|             { students: student }, | ||||
|             { 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'] }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -4,16 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent | |||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||
| 
 | ||||
| export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> { | ||||
|     public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> { | ||||
|     public async findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> { | ||||
|         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 } }); | ||||
|     } | ||||
|     public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> { | ||||
|     public async findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> { | ||||
|         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({ | ||||
|             sender: sender, | ||||
|             receiver: receiver, | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| import { Attachment } from '../../entities/content/attachment.entity.js'; | ||||
| import { Language } from '../../entities/content/language'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; | ||||
| 
 | ||||
| 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({ | ||||
|             learningObject: { | ||||
|                 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( | ||||
|             { | ||||
|                 learningObject: { | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||
| import { Language } from '../../entities/content/language.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||
| 
 | ||||
| export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { | ||||
|     public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { | ||||
|     public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { | ||||
|         return this.findOne( | ||||
|             { | ||||
|                 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( | ||||
|             { | ||||
|                 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( | ||||
|             { admins: teacher }, | ||||
|             { populate: ['admins'] } // Make sure to load admin relations
 | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| import { LearningPath } from '../../entities/content/learning-path.entity.js'; | ||||
| import { Language } from '../../entities/content/language.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| 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'] }); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,12 +1,14 @@ | |||
| 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> { | ||||
|     public async save(entity: T) { | ||||
|         const em = this.getEntityManager(); | ||||
|         em.persist(entity); | ||||
|         await em.flush(); | ||||
|     public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> { | ||||
|         if (options?.preventOverwrite && (await this.findOne(entity))) { | ||||
|             throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); | ||||
|         } | ||||
|         await this.getEntityManager().persistAndFlush(entity); | ||||
|     } | ||||
|     public async deleteWhere(query: FilterQuery<T>) { | ||||
|     public async deleteWhere(query: FilterQuery<T>): Promise<void> { | ||||
|         const toDelete = await this.findOne(query); | ||||
|         const em = this.getEntityManager(); | ||||
|         if (toDelete) { | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js'; | |||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||
| 
 | ||||
| export class AnswerRepository extends DwengoEntityRepository<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({ | ||||
|             toQuestion: answer.toQuestion, | ||||
|             author: answer.author, | ||||
|  | @ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> { | |||
|         }); | ||||
|         return this.insert(answerEntity); | ||||
|     } | ||||
|     public findAllAnswersToQuestion(question: Question): Promise<Answer[]> { | ||||
|     public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> { | ||||
|         return this.findAll({ | ||||
|             where: { toQuestion: question }, | ||||
|             orderBy: { sequenceNumber: 'ASC' }, | ||||
|         }); | ||||
|     } | ||||
|     public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { | ||||
|     public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { | ||||
|         return this.deleteWhere({ | ||||
|             toQuestion: question, | ||||
|             sequenceNumber: sequenceNumber, | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import { Student } from '../../entities/users/student.entity.js'; | |||
| import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||
| 
 | ||||
| export class QuestionRepository extends DwengoEntityRepository<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({ | ||||
|             learningObjectHruid: question.loId.hruid, | ||||
|             learningObjectLanguage: question.loId.language, | ||||
|  | @ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | |||
|         questionEntity.content = question.content; | ||||
|         return this.insert(questionEntity); | ||||
|     } | ||||
|     public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { | ||||
|     public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { | ||||
|         return this.findAll({ | ||||
|             where: { | ||||
|                 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({ | ||||
|             learningObjectHruid: loId.hruid, | ||||
|             learningObjectLanguage: loId.language, | ||||
|  | @ -54,4 +54,11 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | |||
|             orderBy: { timestamp: 'ASC' }, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public async findAllByAuthor(author: Student): Promise<Question[]> { | ||||
|         return this.findAll({ | ||||
|             where: { author }, | ||||
|             orderBy: { timestamp: 'DESC' }, // New to old
 | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -34,8 +34,8 @@ let entityManager: EntityManager | undefined; | |||
| /** | ||||
|  * Execute all the database operations within the function f in a single transaction. | ||||
|  */ | ||||
| export function transactional<T>(f: () => Promise<T>) { | ||||
|     entityManager?.transactional(f); | ||||
| export async function transactional<T>(f: () => Promise<T>): Promise<void> { | ||||
|     await entityManager?.transactional(f); | ||||
| } | ||||
| 
 | ||||
| function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R { | ||||
|  |  | |||
|  | @ -1,7 +1,4 @@ | |||
| export interface Theme { | ||||
|     title: string; | ||||
|     hruids: string[]; | ||||
| } | ||||
| import { Theme } from '@dwengo-1/common/interfaces/theme'; | ||||
| 
 | ||||
| export const themes: Theme[] = [ | ||||
|     { | ||||
|  |  | |||
|  | @ -1,14 +1,11 @@ | |||
| import { Student } from '../../entities/users/student.entity.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> { | ||||
|     public findByUsername(username: string): Promise<Student | null> { | ||||
|     public async findByUsername(username: string): Promise<Student | null> { | ||||
|         return this.findOne({ username: username }); | ||||
|     } | ||||
|     public deleteByUsername(username: string): Promise<void> { | ||||
|     public async deleteByUsername(username: string): Promise<void> { | ||||
|         return this.deleteWhere({ username: username }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ import { Teacher } from '../../entities/users/teacher.entity.js'; | |||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| 
 | ||||
| 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 }); | ||||
|     } | ||||
|     public deleteByUsername(username: string): Promise<void> { | ||||
|     public async deleteByUsername(username: string): Promise<void> { | ||||
|         return this.deleteWhere({ username: username }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | |||
| import { User } from '../../entities/users/user.entity.js'; | ||||
| 
 | ||||
| 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>); | ||||
|     } | ||||
|     public deleteByUsername(username: string): Promise<void> { | ||||
|     public async deleteByUsername(username: string): Promise<void> { | ||||
|         return this.deleteWhere({ username } as Partial<T>); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Class } from '../classes/class.entity.js'; | ||||
| import { Group } from './group.entity.js'; | ||||
| import { Language } from '../content/language.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| import { AssignmentRepository } from '../../data/assignments/assignment-repository.js'; | ||||
| 
 | ||||
| @Entity({ | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import { Student } from '../users/student.entity.js'; | ||||
| import { Group } from './group.entity.js'; | ||||
| import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Language } from '../content/language.js'; | ||||
| import { SubmissionRepository } from '../../data/assignments/submission-repository.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| @Entity({ repository: () => SubmissionRepository }) | ||||
| export class Submission { | ||||
|  | @ -16,7 +16,7 @@ export class Submission { | |||
|     learningObjectLanguage!: Language; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'numeric' }) | ||||
|     learningObjectVersion: number = 1; | ||||
|     learningObjectVersion = 1; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||
|     submissionNumber?: number; | ||||
|  |  | |||
|  | @ -2,12 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; | |||
| import { Student } from '../users/student.entity.js'; | ||||
| import { Class } from './class.entity.js'; | ||||
| import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; | ||||
| 
 | ||||
| export enum ClassJoinRequestStatus { | ||||
|     Open = 'open', | ||||
|     Accepted = 'accepted', | ||||
|     Declined = 'declined', | ||||
| } | ||||
| import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; | ||||
| 
 | ||||
| @Entity({ | ||||
|     repository: () => ClassJoinRequestRepository, | ||||
|  |  | |||
							
								
								
									
										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; | ||||
| } | ||||
|  | @ -1,193 +0,0 @@ | |||
| export enum Language { | ||||
|     Afar = 'aa', | ||||
|     Abkhazian = 'ab', | ||||
|     Afrikaans = 'af', | ||||
|     Akan = 'ak', | ||||
|     Albanian = 'sq', | ||||
|     Amharic = 'am', | ||||
|     Arabic = 'ar', | ||||
|     Aragonese = 'an', | ||||
|     Armenian = 'hy', | ||||
|     Assamese = 'as', | ||||
|     Avaric = 'av', | ||||
|     Avestan = 'ae', | ||||
|     Aymara = 'ay', | ||||
|     Azerbaijani = 'az', | ||||
|     Bashkir = 'ba', | ||||
|     Bambara = 'bm', | ||||
|     Basque = 'eu', | ||||
|     Belarusian = 'be', | ||||
|     Bengali = 'bn', | ||||
|     Bihari = 'bh', | ||||
|     Bislama = 'bi', | ||||
|     Bosnian = 'bs', | ||||
|     Breton = 'br', | ||||
|     Bulgarian = 'bg', | ||||
|     Burmese = 'my', | ||||
|     Catalan = 'ca', | ||||
|     Chamorro = 'ch', | ||||
|     Chechen = 'ce', | ||||
|     Chinese = 'zh', | ||||
|     ChurchSlavic = 'cu', | ||||
|     Chuvash = 'cv', | ||||
|     Cornish = 'kw', | ||||
|     Corsican = 'co', | ||||
|     Cree = 'cr', | ||||
|     Czech = 'cs', | ||||
|     Danish = 'da', | ||||
|     Divehi = 'dv', | ||||
|     Dutch = 'nl', | ||||
|     Dzongkha = 'dz', | ||||
|     English = 'en', | ||||
|     Esperanto = 'eo', | ||||
|     Estonian = 'et', | ||||
|     Ewe = 'ee', | ||||
|     Faroese = 'fo', | ||||
|     Fijian = 'fj', | ||||
|     Finnish = 'fi', | ||||
|     French = 'fr', | ||||
|     Frisian = 'fy', | ||||
|     Fulah = 'ff', | ||||
|     Georgian = 'ka', | ||||
|     German = 'de', | ||||
|     Gaelic = 'gd', | ||||
|     Irish = 'ga', | ||||
|     Galician = 'gl', | ||||
|     Manx = 'gv', | ||||
|     Greek = 'el', | ||||
|     Guarani = 'gn', | ||||
|     Gujarati = 'gu', | ||||
|     Haitian = 'ht', | ||||
|     Hausa = 'ha', | ||||
|     Hebrew = 'he', | ||||
|     Herero = 'hz', | ||||
|     Hindi = 'hi', | ||||
|     HiriMotu = 'ho', | ||||
|     Croatian = 'hr', | ||||
|     Hungarian = 'hu', | ||||
|     Igbo = 'ig', | ||||
|     Icelandic = 'is', | ||||
|     Ido = 'io', | ||||
|     SichuanYi = 'ii', | ||||
|     Inuktitut = 'iu', | ||||
|     Interlingue = 'ie', | ||||
|     Interlingua = 'ia', | ||||
|     Indonesian = 'id', | ||||
|     Inupiaq = 'ik', | ||||
|     Italian = 'it', | ||||
|     Javanese = 'jv', | ||||
|     Japanese = 'ja', | ||||
|     Kalaallisut = 'kl', | ||||
|     Kannada = 'kn', | ||||
|     Kashmiri = 'ks', | ||||
|     Kanuri = 'kr', | ||||
|     Kazakh = 'kk', | ||||
|     Khmer = 'km', | ||||
|     Kikuyu = 'ki', | ||||
|     Kinyarwanda = 'rw', | ||||
|     Kirghiz = 'ky', | ||||
|     Komi = 'kv', | ||||
|     Kongo = 'kg', | ||||
|     Korean = 'ko', | ||||
|     Kuanyama = 'kj', | ||||
|     Kurdish = 'ku', | ||||
|     Lao = 'lo', | ||||
|     Latin = 'la', | ||||
|     Latvian = 'lv', | ||||
|     Limburgan = 'li', | ||||
|     Lingala = 'ln', | ||||
|     Lithuanian = 'lt', | ||||
|     Luxembourgish = 'lb', | ||||
|     LubaKatanga = 'lu', | ||||
|     Ganda = 'lg', | ||||
|     Macedonian = 'mk', | ||||
|     Marshallese = 'mh', | ||||
|     Malayalam = 'ml', | ||||
|     Maori = 'mi', | ||||
|     Marathi = 'mr', | ||||
|     Malay = 'ms', | ||||
|     Malagasy = 'mg', | ||||
|     Maltese = 'mt', | ||||
|     Mongolian = 'mn', | ||||
|     Nauru = 'na', | ||||
|     Navajo = 'nv', | ||||
|     SouthNdebele = 'nr', | ||||
|     NorthNdebele = 'nd', | ||||
|     Ndonga = 'ng', | ||||
|     Nepali = 'ne', | ||||
|     NorwegianNynorsk = 'nn', | ||||
|     NorwegianBokmal = 'nb', | ||||
|     Norwegian = 'no', | ||||
|     Chichewa = 'ny', | ||||
|     Occitan = 'oc', | ||||
|     Ojibwa = 'oj', | ||||
|     Oriya = 'or', | ||||
|     Oromo = 'om', | ||||
|     Ossetian = 'os', | ||||
|     Punjabi = 'pa', | ||||
|     Persian = 'fa', | ||||
|     Pali = 'pi', | ||||
|     Polish = 'pl', | ||||
|     Portuguese = 'pt', | ||||
|     Pashto = 'ps', | ||||
|     Quechua = 'qu', | ||||
|     Romansh = 'rm', | ||||
|     Romanian = 'ro', | ||||
|     Rundi = 'rn', | ||||
|     Russian = 'ru', | ||||
|     Sango = 'sg', | ||||
|     Sanskrit = 'sa', | ||||
|     Sinhala = 'si', | ||||
|     Slovak = 'sk', | ||||
|     Slovenian = 'sl', | ||||
|     NorthernSami = 'se', | ||||
|     Samoan = 'sm', | ||||
|     Shona = 'sn', | ||||
|     Sindhi = 'sd', | ||||
|     Somali = 'so', | ||||
|     Sotho = 'st', | ||||
|     Spanish = 'es', | ||||
|     Sardinian = 'sc', | ||||
|     Serbian = 'sr', | ||||
|     Swati = 'ss', | ||||
|     Sundanese = 'su', | ||||
|     Swahili = 'sw', | ||||
|     Swedish = 'sv', | ||||
|     Tahitian = 'ty', | ||||
|     Tamil = 'ta', | ||||
|     Tatar = 'tt', | ||||
|     Telugu = 'te', | ||||
|     Tajik = 'tg', | ||||
|     Tagalog = 'tl', | ||||
|     Thai = 'th', | ||||
|     Tibetan = 'bo', | ||||
|     Tigrinya = 'ti', | ||||
|     Tonga = 'to', | ||||
|     Tswana = 'tn', | ||||
|     Tsonga = 'ts', | ||||
|     Turkmen = 'tk', | ||||
|     Turkish = 'tr', | ||||
|     Twi = 'tw', | ||||
|     Uighur = 'ug', | ||||
|     Ukrainian = 'uk', | ||||
|     Urdu = 'ur', | ||||
|     Uzbek = 'uz', | ||||
|     Venda = 've', | ||||
|     Vietnamese = 'vi', | ||||
|     Volapuk = 'vo', | ||||
|     Welsh = 'cy', | ||||
|     Walloon = 'wa', | ||||
|     Wolof = 'wo', | ||||
|     Xhosa = 'xh', | ||||
|     Yiddish = 'yi', | ||||
|     Yoruba = 'yo', | ||||
|     Zhuang = 'za', | ||||
|     Zulu = 'zu', | ||||
| } | ||||
| 
 | ||||
| export const languageMap: Record<string, Language> = { | ||||
|     nl: Language.Dutch, | ||||
|     fr: Language.French, | ||||
|     en: Language.English, | ||||
|     de: Language.German, | ||||
| }; | ||||
|  | @ -1,9 +1,11 @@ | |||
| import { Language } from './language.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| export class LearningObjectIdentifier { | ||||
|     constructor( | ||||
|         public hruid: string, | ||||
|         public language: Language, | ||||
|         public version: number | ||||
|     ) {} | ||||
|     ) { | ||||
|         // Do nothing
 | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,28 +1,12 @@ | |||
| import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Language } from './language.js'; | ||||
| import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Attachment } from './attachment.entity.js'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
| import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; | ||||
| import { v4 } from 'uuid'; | ||||
| import { LearningObjectRepository } from '../../data/content/learning-object-repository.js'; | ||||
| 
 | ||||
| @Embeddable() | ||||
| export class EducationalGoal { | ||||
|     @Property({ type: 'string' }) | ||||
|     source!: string; | ||||
| 
 | ||||
|     @Property({ type: 'string' }) | ||||
|     id!: string; | ||||
| } | ||||
| 
 | ||||
| @Embeddable() | ||||
| export class ReturnValue { | ||||
|     @Property({ type: 'string' }) | ||||
|     callbackUrl!: string; | ||||
| 
 | ||||
|     @Property({ type: 'json' }) | ||||
|     callbackSchema!: string; | ||||
| } | ||||
| import { EducationalGoal } from './educational-goal.entity.js'; | ||||
| import { ReturnValue } from './return-value.entity.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| @Entity({ repository: () => LearningObjectRepository }) | ||||
| export class LearningObject { | ||||
|  | @ -36,7 +20,7 @@ export class LearningObject { | |||
|     language!: Language; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'number' }) | ||||
|     version: number = 1; | ||||
|     version = 1; | ||||
| 
 | ||||
|     @Property({ type: 'uuid', unique: true }) | ||||
|     uuid = v4(); | ||||
|  | @ -62,7 +46,7 @@ export class LearningObject { | |||
|     targetAges?: number[] = []; | ||||
| 
 | ||||
|     @Property({ type: 'bool' }) | ||||
|     teacherExclusive: boolean = false; | ||||
|     teacherExclusive = false; | ||||
| 
 | ||||
|     @Property({ type: 'array' }) | ||||
|     skosConcepts: string[] = []; | ||||
|  | @ -74,10 +58,10 @@ export class LearningObject { | |||
|     educationalGoals: EducationalGoal[] = []; | ||||
| 
 | ||||
|     @Property({ type: 'string' }) | ||||
|     copyright: string = ''; | ||||
|     copyright = ''; | ||||
| 
 | ||||
|     @Property({ type: 'string' }) | ||||
|     license: string = ''; | ||||
|     license = ''; | ||||
| 
 | ||||
|     @Property({ type: 'smallint', nullable: true }) | ||||
|     difficulty?: number; | ||||
|  | @ -91,7 +75,7 @@ export class LearningObject { | |||
|     returnValue!: ReturnValue; | ||||
| 
 | ||||
|     @Property({ type: 'bool' }) | ||||
|     available: boolean = true; | ||||
|     available = true; | ||||
| 
 | ||||
|     @Property({ type: 'string', nullable: true }) | ||||
|     contentLocation?: string; | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; | ||||
| import { Language } from './language.js'; | ||||
| import { LearningPath } from './learning-path.entity.js'; | ||||
| import { LearningPathTransition } from './learning-path-transition.entity.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| @Entity() | ||||
| export class LearningPathNode { | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Language } from './language.js'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
| import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; | ||||
| import { LearningPathNode } from './learning-path-node.entity.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| @Entity({ repository: () => LearningPathRepository }) | ||||
| export class LearningPath { | ||||
|  |  | |||
							
								
								
									
										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; | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Language } from '../content/language.js'; | ||||
| import { Student } from '../users/student.entity.js'; | ||||
| import { QuestionRepository } from '../../data/questions/question-repository.js'; | ||||
| import { Language } from '@dwengo-1/common/util/language'; | ||||
| 
 | ||||
| @Entity({ repository: () => QuestionRepository }) | ||||
| export class Question { | ||||
|  | @ -15,7 +15,7 @@ export class Question { | |||
|     learningObjectLanguage!: Language; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'number' }) | ||||
|     learningObjectVersion: number = 1; | ||||
|     learningObjectVersion = 1; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||
|     sequenceNumber?: number; | ||||
|  |  | |||
|  | @ -13,12 +13,4 @@ export class Student extends User { | |||
| 
 | ||||
|     @ManyToMany(() => 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 { | ||||
|     @ManyToMany(() => 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; | ||||
| 
 | ||||
|     @Property() | ||||
|     firstName: string = ''; | ||||
|     firstName = ''; | ||||
| 
 | ||||
|     @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); | ||||
|     } | ||||
| } | ||||
|  | @ -1,14 +1,7 @@ | |||
| import { mapToUserDTO, UserDTO } from './user.js'; | ||||
| import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js'; | ||||
| import { mapToUserDTO } from './user.js'; | ||||
| import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; | ||||
| import { Answer } from '../entities/questions/answer.entity.js'; | ||||
| 
 | ||||
| export interface AnswerDTO { | ||||
|     author: UserDTO; | ||||
|     toQuestion: QuestionDTO; | ||||
|     sequenceNumber: number; | ||||
|     timestamp: string; | ||||
|     content: string; | ||||
| } | ||||
| import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; | ||||
| 
 | ||||
| /** | ||||
|  * Convert a Question entity to a DTO format. | ||||
|  | @ -23,16 +16,10 @@ export function mapToAnswerDTO(answer: Answer): AnswerDTO { | |||
|     }; | ||||
| } | ||||
| 
 | ||||
| export interface AnswerId { | ||||
|     author: string; | ||||
|     toQuestion: QuestionId; | ||||
|     sequenceNumber: number; | ||||
| } | ||||
| 
 | ||||
| export function mapToAnswerId(answer: AnswerDTO): AnswerId { | ||||
| export function mapToAnswerDTOId(answer: Answer): AnswerId { | ||||
|     return { | ||||
|         author: answer.author.username, | ||||
|         toQuestion: mapToQuestionId(answer.toQuestion), | ||||
|         sequenceNumber: answer.sequenceNumber, | ||||
|         toQuestion: mapToQuestionDTOId(answer.toQuestion), | ||||
|         sequenceNumber: answer.sequenceNumber!, | ||||
|     }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,18 +1,9 @@ | |||
| import { languageMap } from '@dwengo-1/common/util/language'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { Assignment } from '../entities/assignments/assignment.entity.js'; | ||||
| import { Class } from '../entities/classes/class.entity.js'; | ||||
| import { languageMap } from '../entities/content/language.js'; | ||||
| import { GroupDTO } from './group.js'; | ||||
| 
 | ||||
| export interface AssignmentDTO { | ||||
|     id: number; | ||||
|     class: string; // Id of class 'within'
 | ||||
|     title: string; | ||||
|     description: string; | ||||
|     learningPath: string; | ||||
|     language: string; | ||||
|     groups?: GroupDTO[] | string[]; // TODO
 | ||||
| } | ||||
| import { getLogger } from '../logging/initalize.js'; | ||||
| import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | ||||
| 
 | ||||
| export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { | ||||
|     return { | ||||
|  | @ -46,5 +37,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi | |||
|     assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; | ||||
|     assignment.within = cls; | ||||
| 
 | ||||
|     getLogger().debug(assignment); | ||||
| 
 | ||||
|     return assignment; | ||||
| } | ||||
|  |  | |||
|  | @ -2,14 +2,7 @@ import { Collection } from '@mikro-orm/core'; | |||
| import { Class } from '../entities/classes/class.entity.js'; | ||||
| import { Student } from '../entities/users/student.entity.js'; | ||||
| import { Teacher } from '../entities/users/teacher.entity.js'; | ||||
| 
 | ||||
| export interface ClassDTO { | ||||
|     id: string; | ||||
|     displayName: string; | ||||
|     teachers: string[]; | ||||
|     students: string[]; | ||||
|     joinRequests: string[]; | ||||
| } | ||||
| import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | ||||
| 
 | ||||
| export function mapToClassDTO(cls: Class): ClassDTO { | ||||
|     return { | ||||
|  |  | |||
|  | @ -1,12 +1,7 @@ | |||
| import { Group } from '../entities/assignments/group.entity.js'; | ||||
| import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js'; | ||||
| import { mapToStudentDTO, StudentDTO } from './student.js'; | ||||
| 
 | ||||
| export interface GroupDTO { | ||||
|     assignment: number | AssignmentDTO; | ||||
|     groupNumber: number; | ||||
|     members: string[] | StudentDTO[]; | ||||
| } | ||||
| import { mapToAssignmentDTO } from './assignment.js'; | ||||
| import { mapToStudentDTO } from './student.js'; | ||||
| import { GroupDTO } from '@dwengo-1/common/interfaces/group'; | ||||
| 
 | ||||
| export function mapToGroupDTO(group: Group): GroupDTO { | ||||
|     return { | ||||
|  |  | |||
|  | @ -1,112 +0,0 @@ | |||
| import { Language } from '../entities/content/language'; | ||||
| 
 | ||||
| export interface Transition { | ||||
|     default: boolean; | ||||
|     _id: string; | ||||
|     next: { | ||||
|         _id: string; | ||||
|         hruid: string; | ||||
|         version: number; | ||||
|         language: string; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectIdentifier { | ||||
|     hruid: string; | ||||
|     language: Language; | ||||
|     version?: number; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectNode { | ||||
|     _id: string; | ||||
|     learningobject_hruid: string; | ||||
|     version: number; | ||||
|     language: Language; | ||||
|     start_node?: boolean; | ||||
|     transitions: Transition[]; | ||||
|     created_at: string; | ||||
|     updatedAt: string; | ||||
|     done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
 | ||||
| } | ||||
| 
 | ||||
| export interface LearningPath { | ||||
|     _id: string; | ||||
|     language: string; | ||||
|     hruid: string; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     image?: string; // Image might be missing, so it's optional
 | ||||
|     num_nodes: number; | ||||
|     num_nodes_left: number; | ||||
|     nodes: LearningObjectNode[]; | ||||
|     keywords: string; | ||||
|     target_ages: number[]; | ||||
|     min_age: number; | ||||
|     max_age: number; | ||||
|     __order: number; | ||||
| } | ||||
| 
 | ||||
| export interface LearningPathIdentifier { | ||||
|     hruid: string; | ||||
|     language: Language; | ||||
| } | ||||
| 
 | ||||
| export interface EducationalGoal { | ||||
|     source: string; | ||||
|     id: string; | ||||
| } | ||||
| 
 | ||||
| export interface ReturnValue { | ||||
|     callback_url: string; | ||||
|     callback_schema: Record<string, any>; | ||||
| } | ||||
| 
 | ||||
| export interface LearningObjectMetadata { | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     hruid: string; | ||||
|     version: number; | ||||
|     language: Language; | ||||
|     title: string; | ||||
|     description: string; | ||||
|     difficulty: number; | ||||
|     estimated_time: number; | ||||
|     available: boolean; | ||||
|     teacher_exclusive: boolean; | ||||
|     educational_goals: EducationalGoal[]; | ||||
|     keywords: string[]; | ||||
|     target_ages: number[]; | ||||
|     content_type: string; // Markdown, image, etc.
 | ||||
|     content_location?: string; | ||||
|     skos_concepts?: string[]; | ||||
|     return_value?: ReturnValue; | ||||
| } | ||||
| 
 | ||||
| export interface FilteredLearningObject { | ||||
|     key: string; | ||||
|     _id: string; | ||||
|     uuid: string; | ||||
|     version: number; | ||||
|     title: string; | ||||
|     htmlUrl: string; | ||||
|     language: Language; | ||||
|     difficulty: number; | ||||
|     estimatedTime?: number; | ||||
|     available: boolean; | ||||
|     teacherExclusive: boolean; | ||||
|     educationalGoals: EducationalGoal[]; | ||||
|     keywords: string[]; | ||||
|     description: string; | ||||
|     targetAges: number[]; | ||||
|     contentType: string; | ||||
|     contentLocation?: string; | ||||
|     skosConcepts?: string[]; | ||||
|     returnValue?: ReturnValue; | ||||
| } | ||||
| 
 | ||||
| export interface LearningPathResponse { | ||||
|     success: boolean; | ||||
|     source: string; | ||||
|     data: LearningPath[] | null; | ||||
|     message?: string; | ||||
| } | ||||
|  | @ -1,24 +1,21 @@ | |||
| import { Question } from '../entities/questions/question.entity.js'; | ||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||
| import { mapToStudentDTO, StudentDTO } from './student.js'; | ||||
| import { mapToStudentDTO } from './student.js'; | ||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||
| import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| 
 | ||||
| export interface QuestionDTO { | ||||
|     learningObjectIdentifier: LearningObjectIdentifier; | ||||
|     sequenceNumber?: number; | ||||
|     author: StudentDTO; | ||||
|     timestamp?: string; | ||||
|     content: string; | ||||
| function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { | ||||
|     return { | ||||
|         hruid: question.learningObjectHruid, | ||||
|         language: question.learningObjectLanguage, | ||||
|         version: question.learningObjectVersion, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Convert a Question entity to a DTO format. | ||||
|  */ | ||||
| export function mapToQuestionDTO(question: Question): QuestionDTO { | ||||
|     const learningObjectIdentifier = { | ||||
|         hruid: question.learningObjectHruid, | ||||
|         language: question.learningObjectLanguage, | ||||
|         version: question.learningObjectVersion, | ||||
|     }; | ||||
|     const learningObjectIdentifier = getLearningObjectIdentifier(question); | ||||
| 
 | ||||
|     return { | ||||
|         learningObjectIdentifier, | ||||
|  | @ -29,14 +26,11 @@ export function mapToQuestionDTO(question: Question): QuestionDTO { | |||
|     }; | ||||
| } | ||||
| 
 | ||||
| export interface QuestionId { | ||||
|     learningObjectIdentifier: LearningObjectIdentifier; | ||||
|     sequenceNumber: number; | ||||
| } | ||||
| export function mapToQuestionDTOId(question: Question): QuestionId { | ||||
|     const learningObjectIdentifier = getLearningObjectIdentifier(question); | ||||
| 
 | ||||
| export function mapToQuestionId(question: QuestionDTO): QuestionId { | ||||
|     return { | ||||
|         learningObjectIdentifier: question.learningObjectIdentifier, | ||||
|         learningObjectIdentifier, | ||||
|         sequenceNumber: question.sequenceNumber!, | ||||
|     }; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										23
									
								
								backend/src/interfaces/student-request.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								backend/src/interfaces/student-request.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| import { mapToStudentDTO } from './student.js'; | ||||
| import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js'; | ||||
| import { getClassJoinRequestRepository } from '../data/repositories.js'; | ||||
| import { Student } from '../entities/users/student.entity.js'; | ||||
| import { Class } from '../entities/classes/class.entity.js'; | ||||
| import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | ||||
| import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; | ||||
| 
 | ||||
| export function mapToStudentRequestDTO(request: ClassJoinRequest): ClassJoinRequestDTO { | ||||
|     return { | ||||
|         requester: mapToStudentDTO(request.requester), | ||||
|         class: request.class.classId!, | ||||
|         status: request.status, | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export function mapToStudentRequest(student: Student, cls: Class): ClassJoinRequest { | ||||
|     return getClassJoinRequestRepository().create({ | ||||
|         requester: student, | ||||
|         class: cls, | ||||
|         status: ClassJoinRequestStatus.Open, | ||||
|     }); | ||||
| } | ||||
|  | @ -1,17 +1,6 @@ | |||
| import { Student } from '../entities/users/student.entity.js'; | ||||
| 
 | ||||
| export interface StudentDTO { | ||||
|     id: string; | ||||
|     username: string; | ||||
|     firstName: string; | ||||
|     lastName: string; | ||||
|     endpoints?: { | ||||
|         classes: string; | ||||
|         questions: string; | ||||
|         invitations: string; | ||||
|         groups: string; | ||||
|     }; | ||||
| } | ||||
| import { getStudentRepository } from '../data/repositories.js'; | ||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||
| 
 | ||||
| export function mapToStudentDTO(student: Student): StudentDTO { | ||||
|     return { | ||||
|  | @ -23,7 +12,9 @@ export function mapToStudentDTO(student: Student): StudentDTO { | |||
| } | ||||
| 
 | ||||
| export function mapToStudent(studentData: StudentDTO): Student { | ||||
|     const student = new Student(studentData.username, studentData.firstName, studentData.lastName); | ||||
| 
 | ||||
|     return student; | ||||
|     return getStudentRepository().create({ | ||||
|         username: studentData.username, | ||||
|         firstName: studentData.firstName, | ||||
|         lastName: studentData.lastName, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -1,26 +1,7 @@ | |||
| import { Submission } from '../entities/assignments/submission.entity.js'; | ||||
| import { Language } from '../entities/content/language.js'; | ||||
| import { GroupDTO, mapToGroupDTO } from './group.js'; | ||||
| import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; | ||||
| import { LearningObjectIdentifier } from './learning-content.js'; | ||||
| 
 | ||||
| export interface SubmissionDTO { | ||||
|     learningObjectIdentifier: LearningObjectIdentifier; | ||||
| 
 | ||||
|     submissionNumber?: number; | ||||
|     submitter: StudentDTO; | ||||
|     time?: Date; | ||||
|     group?: GroupDTO; | ||||
|     content: string; | ||||
| } | ||||
| 
 | ||||
| export interface SubmissionDTOId { | ||||
|     learningObjectHruid: string; | ||||
|     learningObjectLanguage: Language; | ||||
|     learningObjectVersion: number; | ||||
| 
 | ||||
|     submissionNumber?: number; | ||||
| } | ||||
| import { mapToGroupDTO } from './group.js'; | ||||
| import { mapToStudent, mapToStudentDTO } from './student.js'; | ||||
| import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | ||||
| 
 | ||||
| export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { | ||||
|     return { | ||||
|  |  | |||
|  | @ -1,12 +1,7 @@ | |||
| import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; | ||||
| import { ClassDTO, mapToClassDTO } from './class.js'; | ||||
| import { mapToUserDTO, UserDTO } from './user.js'; | ||||
| 
 | ||||
| export interface TeacherInvitationDTO { | ||||
|     sender: string | UserDTO; | ||||
|     receiver: string | UserDTO; | ||||
|     class: string | ClassDTO; | ||||
| } | ||||
| import { mapToClassDTO } from './class.js'; | ||||
| import { mapToUserDTO } from './user.js'; | ||||
| import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; | ||||
| 
 | ||||
| export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { | ||||
|     return { | ||||
|  |  | |||
|  | @ -1,17 +1,6 @@ | |||
| import { Teacher } from '../entities/users/teacher.entity.js'; | ||||
| 
 | ||||
| export interface TeacherDTO { | ||||
|     id: string; | ||||
|     username: string; | ||||
|     firstName: string; | ||||
|     lastName: string; | ||||
|     endpoints?: { | ||||
|         classes: string; | ||||
|         questions: string; | ||||
|         invitations: string; | ||||
|         groups: string; | ||||
|     }; | ||||
| } | ||||
| import { getTeacherRepository } from '../data/repositories.js'; | ||||
| import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||
| 
 | ||||
| export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { | ||||
|     return { | ||||
|  | @ -22,8 +11,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { | |||
|     }; | ||||
| } | ||||
| 
 | ||||
| export function mapToTeacher(TeacherData: TeacherDTO): Teacher { | ||||
|     const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName); | ||||
| 
 | ||||
|     return teacher; | ||||
| export function mapToTeacher(teacherData: TeacherDTO): Teacher { | ||||
|     return getTeacherRepository().create({ | ||||
|         username: teacherData.username, | ||||
|         firstName: teacherData.firstName, | ||||
|         lastName: teacherData.lastName, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -1,17 +1,5 @@ | |||
| import { User } from '../entities/users/user.entity.js'; | ||||
| 
 | ||||
| export interface UserDTO { | ||||
|     id?: string; | ||||
|     username: string; | ||||
|     firstName: string; | ||||
|     lastName: string; | ||||
|     endpoints?: { | ||||
|         self: string; | ||||
|         classes: string; | ||||
|         questions: string; | ||||
|         invitations: string; | ||||
|     }; | ||||
| } | ||||
| import { UserDTO } from '@dwengo-1/common/interfaces/user'; | ||||
| 
 | ||||
| export function mapToUserDTO(user: User): UserDTO { | ||||
|     return { | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; | ||||
| import LokiTransport from 'winston-loki'; | ||||
| import { LokiLabels } from 'loki-logger-ts'; | ||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||
| import { envVars, getEnvVar } from '../util/envVars.js'; | ||||
| 
 | ||||
| export class Logger extends WinstonLogger { | ||||
|     constructor() { | ||||
|  | @ -9,7 +9,7 @@ export class Logger extends WinstonLogger { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| const Labels: LokiLabels = { | ||||
| const lokiLabels: LokiLabels = { | ||||
|     source: 'Dwengo-Backend', | ||||
|     service: 'API', | ||||
|     host: 'localhost', | ||||
|  | @ -22,28 +22,28 @@ function initializeLogger(): Logger { | |||
|         return logger; | ||||
|     } | ||||
| 
 | ||||
|     const logLevel = getEnvVar(EnvVars.LogLevel); | ||||
|     const logLevel = getEnvVar(envVars.LogLevel); | ||||
| 
 | ||||
|     const consoleTransport = new transports.Console({ | ||||
|         level: getEnvVar(EnvVars.LogLevel), | ||||
|         level: getEnvVar(envVars.LogLevel), | ||||
|         format: format.combine(format.cli(), format.colorize()), | ||||
|     }); | ||||
| 
 | ||||
|     if (getEnvVar(EnvVars.RunMode) === 'dev') { | ||||
|     if (getEnvVar(envVars.RunMode) === 'dev') { | ||||
|         return createLogger({ | ||||
|             transports: [consoleTransport], | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     const lokiHost = getEnvVar(EnvVars.LokiHost); | ||||
|     const lokiHost = getEnvVar(envVars.LokiHost); | ||||
| 
 | ||||
|     const lokiTransport: LokiTransport = new LokiTransport({ | ||||
|         host: lokiHost, | ||||
|         labels: Labels, | ||||
|         labels: lokiLabels, | ||||
|         level: logLevel, | ||||
|         json: true, | ||||
|         format: format.combine(format.timestamp(), format.json()), | ||||
|         onConnectionError: (err) => { | ||||
|         onConnectionError: (err): void => { | ||||
|             // eslint-disable-next-line no-console
 | ||||
|             console.error(`Connection error: ${err}`); | ||||
|         }, | ||||
|  |  | |||
|  | @ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts'; | |||
| export class MikroOrmLogger extends DefaultLogger { | ||||
|     private logger: Logger = getLogger(); | ||||
| 
 | ||||
|     log(namespace: LoggerNamespace, message: string, context?: LogContext) { | ||||
|     static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown { | ||||
|         const labels: LokiLabels = { | ||||
|             service: 'ORM', | ||||
|         }; | ||||
| 
 | ||||
|         let message: string; | ||||
|         if (context?.label) { | ||||
|             message = `[${namespace}] (${context.label}) ${messageArg}`; | ||||
|         } else { | ||||
|             message = `[${namespace}] ${messageArg}`; | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             message: message, | ||||
|             labels: labels, | ||||
|             context: context, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     log(namespace: LoggerNamespace, message: string, context?: LogContext): void { | ||||
|         if (!this.isEnabled(namespace, context)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         switch (namespace) { | ||||
|             case 'query': | ||||
|                 this.logger.debug(this.createMessage(namespace, message, context)); | ||||
|                 this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'query-params': | ||||
|                 // TODO Which log level should this be?
 | ||||
|                 this.logger.info(this.createMessage(namespace, message, context)); | ||||
|                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'schema': | ||||
|                 this.logger.info(this.createMessage(namespace, message, context)); | ||||
|                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'discovery': | ||||
|                 this.logger.debug(this.createMessage(namespace, message, context)); | ||||
|                 this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'info': | ||||
|                 this.logger.info(this.createMessage(namespace, message, context)); | ||||
|                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'deprecated': | ||||
|                 this.logger.warn(this.createMessage(namespace, message, context)); | ||||
|                 this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             default: | ||||
|                 switch (context?.level) { | ||||
|                     case 'info': | ||||
|                         this.logger.info(this.createMessage(namespace, message, context)); | ||||
|                         this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||
|                         break; | ||||
|                     case 'warning': | ||||
|                         this.logger.warn(message); | ||||
|  | @ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger { | |||
|                 } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) { | ||||
|         const labels: LokiLabels = { | ||||
|             service: 'ORM', | ||||
|         }; | ||||
| 
 | ||||
|         let message: string; | ||||
|         if (context?.label) { | ||||
|             message = `[${namespace}] (${context?.label}) ${messageArg}`; | ||||
|         } else { | ||||
|             message = `[${namespace}] ${messageArg}`; | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             message: message, | ||||
|             labels: labels, | ||||
|             context: context, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { getLogger, Logger } from './initalize.js'; | ||||
| import { Request, Response } from 'express'; | ||||
| 
 | ||||
| export function responseTimeLogger(req: Request, res: Response, time: number) { | ||||
| export function responseTimeLogger(req: Request, res: Response, time: number): void { | ||||
|     const logger: Logger = getLogger(); | ||||
| 
 | ||||
|     const method = req.method; | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| import { EnvVars, getEnvVar } from '../../util/envvars.js'; | ||||
| import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||
| import { expressjwt } from 'express-jwt'; | ||||
| import * as jwt from 'jsonwebtoken'; | ||||
| import { JwtPayload } from 'jsonwebtoken'; | ||||
| import jwksClient from 'jwks-rsa'; | ||||
| import * as express from 'express'; | ||||
| import * as jwt from 'jsonwebtoken'; | ||||
| import { AuthenticatedRequest } from './authenticated-request.js'; | ||||
| import { AuthenticationInfo } from './authentication-info.js'; | ||||
| import { ForbiddenException, UnauthorizedException } from '../../exceptions.js'; | ||||
| import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; | ||||
| import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; | ||||
| 
 | ||||
| const JWKS_CACHE = true; | ||||
| const JWKS_RATE_LIMIT = true; | ||||
|  | @ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient { | |||
| 
 | ||||
| const idpConfigs = { | ||||
|     student: { | ||||
|         issuer: getEnvVar(EnvVars.IdpStudentUrl), | ||||
|         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), | ||||
|         issuer: getEnvVar(envVars.IdpStudentUrl), | ||||
|         jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)), | ||||
|     }, | ||||
|     teacher: { | ||||
|         issuer: getEnvVar(EnvVars.IdpTeacherUrl), | ||||
|         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), | ||||
|         issuer: getEnvVar(envVars.IdpTeacherUrl), | ||||
|         jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)), | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
|  | @ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({ | |||
|         } | ||||
|         return signingKey.getPublicKey(); | ||||
|     }, | ||||
|     audience: getEnvVar(EnvVars.IdpAudience), | ||||
|     audience: getEnvVar(envVars.IdpAudience), | ||||
|     algorithms: [JWT_ALGORITHM], | ||||
|     credentialsRequired: false, | ||||
|     requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, | ||||
|  | @ -74,7 +75,7 @@ const verifyJwtToken = expressjwt({ | |||
|  */ | ||||
| function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { | ||||
|     if (!req.jwtPayload) { | ||||
|         return; | ||||
|         return undefined; | ||||
|     } | ||||
|     const issuer = req.jwtPayload.iss; | ||||
|     let accountType: 'student' | 'teacher'; | ||||
|  | @ -84,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | | |||
|     } else if (issuer === idpConfigs.teacher.issuer) { | ||||
|         accountType = 'teacher'; | ||||
|     } else { | ||||
|         return; | ||||
|         return undefined; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         accountType: accountType, | ||||
|         username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, | ||||
|  | @ -100,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | | |||
|  * Add the AuthenticationInfo object with the information about the current authentication to the request in order | ||||
|  * to avoid that the routers have to deal with the JWT token. | ||||
|  */ | ||||
| const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => { | ||||
| function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void { | ||||
|     req.auth = getAuthenticationInfo(req); | ||||
|     next(); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | ||||
| 
 | ||||
|  | @ -113,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | |||
|  * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates | ||||
|  *                        to true. | ||||
|  */ | ||||
| export const authorize = | ||||
|     (accessCondition: (auth: AuthenticationInfo) => boolean) => | ||||
|     (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => { | ||||
| export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) { | ||||
|     return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => { | ||||
|         if (!req.auth) { | ||||
|             throw new UnauthorizedException(); | ||||
|         } else if (!accessCondition(req.auth)) { | ||||
|  | @ -124,6 +125,7 @@ export const authorize = | |||
|             next(); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Middleware which rejects all unauthenticated users, but accepts all authenticated users. | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| /** | ||||
|  * Object with information about the user who is currently logged in. | ||||
|  */ | ||||
| export type AuthenticationInfo = { | ||||
| export interface AuthenticationInfo { | ||||
|     accountType: 'student' | 'teacher'; | ||||
|     username: string; | ||||
|     name?: string; | ||||
|     firstName?: string; | ||||
|     lastName?: string; | ||||
|     email?: string; | ||||
| }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import cors from 'cors'; | ||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||
| import { envVars, getEnvVar } from '../util/envVars.js'; | ||||
| 
 | ||||
| export default cors({ | ||||
|     origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','), | ||||
|     allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','), | ||||
|     origin: getEnvVar(envVars.CorsAllowedOrigins).split(','), | ||||
|     allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','), | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										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 { PostgreSqlDriver } from '@mikro-orm/postgresql'; | ||||
| import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; | ||||
| import { envVars, getEnvVar, getNumericEnvVar } from './util/envVars.js'; | ||||
| import { SqliteDriver } from '@mikro-orm/sqlite'; | ||||
| import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | ||||
| 
 | ||||
|  | @ -42,33 +42,35 @@ const entities = [ | |||
|     Question, | ||||
| ]; | ||||
| 
 | ||||
| function config(testingMode: boolean = false): Options { | ||||
| function config(testingMode = false): Options { | ||||
|     if (testingMode) { | ||||
|         return { | ||||
|             driver: SqliteDriver, | ||||
|             dbName: getEnvVar(EnvVars.DbName), | ||||
|             dbName: getEnvVar(envVars.DbName), | ||||
|             subscribers: [new SqliteAutoincrementSubscriber()], | ||||
|             entities: entities, | ||||
|             persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
 | ||||
|             // EntitiesTs: entitiesTs,
 | ||||
| 
 | ||||
|             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 | ||||
|             // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
 | ||||
|             dynamicImportProvider: (id) => import(id), | ||||
|             dynamicImportProvider: async (id) => import(id), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         driver: PostgreSqlDriver, | ||||
|         host: getEnvVar(EnvVars.DbHost), | ||||
|         port: getNumericEnvVar(EnvVars.DbPort), | ||||
|         dbName: getEnvVar(EnvVars.DbName), | ||||
|         user: getEnvVar(EnvVars.DbUsername), | ||||
|         password: getEnvVar(EnvVars.DbPassword), | ||||
|         host: getEnvVar(envVars.DbHost), | ||||
|         port: getNumericEnvVar(envVars.DbPort), | ||||
|         dbName: getEnvVar(envVars.DbName), | ||||
|         user: getEnvVar(envVars.DbUsername), | ||||
|         password: getEnvVar(envVars.DbPassword), | ||||
|         entities: entities, | ||||
|         persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
 | ||||
|         // EntitiesTs: entitiesTs,
 | ||||
| 
 | ||||
|         // Logging
 | ||||
|         debug: getEnvVar(EnvVars.LogLevel) === 'debug', | ||||
|         debug: getEnvVar(envVars.LogLevel) === 'debug', | ||||
|         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), | ||||
|     }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import { EntityManager, MikroORM } from '@mikro-orm/core'; | ||||
| import config from './mikro-orm.config.js'; | ||||
| import { EnvVars, getEnvVar } from './util/envvars.js'; | ||||
| import { envVars, getEnvVar } from './util/envVars.js'; | ||||
| import { getLogger, Logger } from './logging/initalize.js'; | ||||
| 
 | ||||
| let orm: MikroORM | undefined; | ||||
| export async function initORM(testingMode: boolean = false) { | ||||
| export async function initORM(testingMode = false): Promise<void> { | ||||
|     const logger: Logger = getLogger(); | ||||
| 
 | ||||
|     logger.info('Initializing ORM'); | ||||
|  | @ -12,7 +12,7 @@ export async function initORM(testingMode: boolean = false) { | |||
| 
 | ||||
|     orm = await MikroORM.init(config(testingMode)); | ||||
|     // Update the database scheme if necessary and enabled.
 | ||||
|     if (getEnvVar(EnvVars.DbUpdate)) { | ||||
|     if (getEnvVar(envVars.DbUpdate)) { | ||||
|         await orm.schema.updateSchema(); | ||||
|     } else { | ||||
|         const diff = await orm.schema.getUpdateSchemaSQL(); | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ router.get('/:id', getAssignmentHandler); | |||
| 
 | ||||
| router.get('/:id/submissions', getAssignmentsSubmissionsHandler); | ||||
| 
 | ||||
| router.get('/:id/questions', (req, res) => { | ||||
| router.get('/:id/questions', (_req, res) => { | ||||
|     res.json({ | ||||
|         questions: ['0'], | ||||
|     }); | ||||
|  |  | |||
|  | @ -4,21 +4,21 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut | |||
| const router = express.Router(); | ||||
| 
 | ||||
| // Returns auth configuration for frontend
 | ||||
| router.get('/config', (req, res) => { | ||||
| router.get('/config', (_req, res) => { | ||||
|     res.json(getFrontendAuthConfig()); | ||||
| }); | ||||
| 
 | ||||
| router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => { | ||||
| router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { | ||||
|     /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ | ||||
|     res.json({ message: 'If you see this, you should be authenticated!' }); | ||||
| }); | ||||
| 
 | ||||
| router.get('/testStudentsOnly', studentsOnly, (req, res) => { | ||||
| router.get('/testStudentsOnly', studentsOnly, (_req, res) => { | ||||
|     /* #swagger.security = [{ "student": [ ] }] */ | ||||
|     res.json({ message: 'If you see this, you should be a student!' }); | ||||
| }); | ||||
| 
 | ||||
| router.get('/testTeachersOnly', teachersOnly, (req, res) => { | ||||
| router.get('/testTeachersOnly', teachersOnly, (_req, res) => { | ||||
|     /* #swagger.security = [{ "teacher": [ ] }] */ | ||||
|     res.json({ message: 'If you see this, you should be a teacher!' }); | ||||
| }); | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ router.get('/:groupid', getGroupHandler); | |||
| router.get('/:groupid/submissions', getGroupSubmissionsHandler); | ||||
| 
 | ||||
| // The list of questions a group has made
 | ||||
| router.get('/:id/questions', (req, res) => { | ||||
| router.get('/:id/questions', (_req, res) => { | ||||
|     res.json({ | ||||
|         questions: ['0'], | ||||
|     }); | ||||
|  |  | |||
							
								
								
									
										19
									
								
								backend/src/routes/student-join-requests.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								backend/src/routes/student-join-requests.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| import express from 'express'; | ||||
| import { | ||||
|     createStudentRequestHandler, | ||||
|     deleteClassJoinRequestHandler, | ||||
|     getStudentRequestHandler, | ||||
|     getStudentRequestsHandler, | ||||
| } from '../controllers/students.js'; | ||||
| 
 | ||||
| const router = express.Router({ mergeParams: true }); | ||||
| 
 | ||||
| router.get('/', getStudentRequestsHandler); | ||||
| 
 | ||||
| router.post('/', createStudentRequestHandler); | ||||
| 
 | ||||
| router.get('/:classId', getStudentRequestHandler); | ||||
| 
 | ||||
| router.delete('/:classId', deleteClassJoinRequestHandler); | ||||
| 
 | ||||
| export default router; | ||||
|  | @ -7,8 +7,11 @@ import { | |||
|     getStudentClassesHandler, | ||||
|     getStudentGroupsHandler, | ||||
|     getStudentHandler, | ||||
|     getStudentQuestionsHandler, | ||||
|     getStudentSubmissionsHandler, | ||||
| } from '../controllers/students.js'; | ||||
| import joinRequestRouter from './student-join-requests.js'; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // Root endpoint used to search objects
 | ||||
|  | @ -16,30 +19,26 @@ router.get('/', getAllStudentsHandler); | |||
| 
 | ||||
| router.post('/', createStudentHandler); | ||||
| 
 | ||||
| router.delete('/', deleteStudentHandler); | ||||
| 
 | ||||
| router.delete('/:username', deleteStudentHandler); | ||||
| 
 | ||||
| // Information about a student's profile
 | ||||
| router.get('/:username', getStudentHandler); | ||||
| 
 | ||||
| // The list of classes a student is in
 | ||||
| router.get('/:id/classes', getStudentClassesHandler); | ||||
| router.get('/:username/classes', getStudentClassesHandler); | ||||
| 
 | ||||
| // The list of submissions a student has made
 | ||||
| router.get('/:id/submissions', getStudentSubmissionsHandler); | ||||
| router.get('/:username/submissions', getStudentSubmissionsHandler); | ||||
| 
 | ||||
| // The list of assignments a student has
 | ||||
| router.get('/:id/assignments', getStudentAssignmentsHandler); | ||||
| router.get('/:username/assignments', getStudentAssignmentsHandler); | ||||
| 
 | ||||
| // The list of groups a student is in
 | ||||
| router.get('/:id/groups', getStudentGroupsHandler); | ||||
| router.get('/:username/groups', getStudentGroupsHandler); | ||||
| 
 | ||||
| // A list of questions a user has created
 | ||||
| router.get('/:id/questions', (req, res) => { | ||||
|     res.json({ | ||||
|         questions: ['0'], | ||||
|     }); | ||||
| }); | ||||
| router.get('/:username/questions', getStudentQuestionsHandler); | ||||
| 
 | ||||
| router.use('/:username/joinRequests', joinRequestRouter); | ||||
| 
 | ||||
| export default router; | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler | |||
| const router = express.Router({ mergeParams: true }); | ||||
| 
 | ||||
| // Root endpoint used to search objects
 | ||||
| router.get('/', (req, res) => { | ||||
| router.get('/', (_req, res) => { | ||||
|     res.json({ | ||||
|         submissions: ['0', '1'], | ||||
|     }); | ||||
|  |  | |||
|  | @ -3,10 +3,12 @@ import { | |||
|     createTeacherHandler, | ||||
|     deleteTeacherHandler, | ||||
|     getAllTeachersHandler, | ||||
|     getStudentJoinRequestHandler, | ||||
|     getTeacherClassHandler, | ||||
|     getTeacherHandler, | ||||
|     getTeacherQuestionHandler, | ||||
|     getTeacherStudentHandler, | ||||
|     updateStudentJoinRequestHandler, | ||||
| } from '../controllers/teachers.js'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
|  | @ -15,8 +17,6 @@ router.get('/', getAllTeachersHandler); | |||
| 
 | ||||
| router.post('/', createTeacherHandler); | ||||
| 
 | ||||
| router.delete('/', deleteTeacherHandler); | ||||
| 
 | ||||
| router.get('/:username', getTeacherHandler); | ||||
| 
 | ||||
| router.delete('/:username', deleteTeacherHandler); | ||||
|  | @ -27,8 +27,12 @@ router.get('/:username/students', getTeacherStudentHandler); | |||
| 
 | ||||
| router.get('/:username/questions', getTeacherQuestionHandler); | ||||
| 
 | ||||
| router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); | ||||
| 
 | ||||
| router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); | ||||
| 
 | ||||
| // Invitations to other classes a teacher received
 | ||||
| router.get('/:id/invitations', (req, res) => { | ||||
| router.get('/:id/invitations', (_req, res) => { | ||||
|     res.json({ | ||||
|         invitations: ['0'], | ||||
|     }); | ||||
|  |  | |||
|  | @ -1,6 +1,9 @@ | |||
| import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; | ||||
| import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; | ||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | ||||
| import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; | ||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; | ||||
| import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | ||||
| import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | ||||
| import { getLogger } from '../logging/initalize.js'; | ||||
| 
 | ||||
| export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> { | ||||
|     const classRepository = getClassRepository(); | ||||
|  | @ -37,7 +40,7 @@ export async function createAssignment(classid: string, assignmentData: Assignme | |||
| 
 | ||||
|         return mapToAssignmentDTO(newAssignment); | ||||
|     } catch (e) { | ||||
|         console.error(e); | ||||
|         getLogger().error(e); | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | @ -83,7 +86,7 @@ export async function getAssignmentsSubmissions( | |||
|     const groups = await groupRepository.findAllGroupsForAssignment(assignment); | ||||
| 
 | ||||
|     const submissionRepository = getSubmissionRepository(); | ||||
|     const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); | ||||
|     const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); | ||||
| 
 | ||||
|     if (full) { | ||||
|         return submissions.map(mapToSubmissionDTO); | ||||
|  |  | |||
|  | @ -1,11 +1,27 @@ | |||
| import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; | ||||
| import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | ||||
| import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; | ||||
| import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; | ||||
| import { mapToClassDTO } from '../interfaces/class.js'; | ||||
| import { mapToStudentDTO } from '../interfaces/student.js'; | ||||
| import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js'; | ||||
| import { getLogger } from '../logging/initalize.js'; | ||||
| import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||
| import { Class } from '../entities/classes/class.entity.js'; | ||||
| import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | ||||
| import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; | ||||
| import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||
| 
 | ||||
| const logger = getLogger(); | ||||
| 
 | ||||
| export async function fetchClass(classId: string): Promise<Class> { | ||||
|     const classRepository = getClassRepository(); | ||||
|     const cls = await classRepository.findById(classId); | ||||
| 
 | ||||
|     if (!cls) { | ||||
|         throw new NotFoundException('Class with id not found'); | ||||
|     } | ||||
| 
 | ||||
|     return cls; | ||||
| } | ||||
| 
 | ||||
| export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> { | ||||
|     const classRepository = getClassRepository(); | ||||
|     const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); | ||||
|  | @ -23,11 +39,15 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[ | |||
| export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> { | ||||
|     const teacherRepository = getTeacherRepository(); | ||||
|     const teacherUsernames = classData.teachers || []; | ||||
|     const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null); | ||||
|     const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter( | ||||
|         (teacher) => teacher !== null | ||||
|     ); | ||||
| 
 | ||||
|     const studentRepository = getStudentRepository(); | ||||
|     const studentUsernames = classData.students || []; | ||||
|     const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); | ||||
|     const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter( | ||||
|         (student) => student !== null | ||||
|     ); | ||||
| 
 | ||||
|     const classRepository = getClassRepository(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,8 +6,11 @@ import { | |||
|     getSubmissionRepository, | ||||
| } from '../data/repositories.js'; | ||||
| import { Group } from '../entities/assignments/group.entity.js'; | ||||
| import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | ||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; | ||||
| import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; | ||||
| import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; | ||||
| import { GroupDTO } from '@dwengo-1/common/interfaces/group'; | ||||
| import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | ||||
| import { getLogger } from '../logging/initalize.js'; | ||||
| 
 | ||||
| export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> { | ||||
|     const classRepository = getClassRepository(); | ||||
|  | @ -42,9 +45,11 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme | |||
|     const studentRepository = getStudentRepository(); | ||||
| 
 | ||||
|     const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
 | ||||
|     const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); | ||||
|     const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter( | ||||
|         (student) => student !== null | ||||
|     ); | ||||
| 
 | ||||
|     console.log(members); | ||||
|     getLogger().debug(members); | ||||
| 
 | ||||
|     const classRepository = getClassRepository(); | ||||
|     const cls = await classRepository.findById(classid); | ||||
|  | @ -70,7 +75,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme | |||
| 
 | ||||
|         return newGroup; | ||||
|     } catch (e) { | ||||
|         console.log(e); | ||||
|         getLogger().error(e); | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | @ -94,8 +99,7 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu | |||
|     const groups = await groupRepository.findAllGroupsForAssignment(assignment); | ||||
| 
 | ||||
|     if (full) { | ||||
|         console.log('full'); | ||||
|         console.log(groups); | ||||
|         getLogger().debug({ full: full, groups: groups }); | ||||
|         return groups.map(mapToGroupDTO); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,13 @@ | |||
| import { DWENGO_API_BASE } from '../config.js'; | ||||
| import { fetchWithLogging } from '../util/api-helper.js'; | ||||
| import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; | ||||
| 
 | ||||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectMetadata, | ||||
|     LearningObjectNode, | ||||
|     LearningPathResponse, | ||||
| } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import { getLogger } from '../logging/initalize.js'; | ||||
| 
 | ||||
| function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { | ||||
|     return { | ||||
|  | @ -37,7 +44,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr | |||
|     ); | ||||
| 
 | ||||
|     if (!metadata) { | ||||
|         console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); | ||||
|         getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`); | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|  | @ -48,7 +55,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr | |||
| /** | ||||
|  * 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.'); | ||||
| } | ||||
| 
 | ||||
|  | @ -60,7 +67,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri | |||
|         const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`); | ||||
| 
 | ||||
|         if (!learningPathResponse.success || !learningPathResponse.data?.length) { | ||||
|             console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); | ||||
|             getLogger().error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|  | @ -74,7 +81,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri | |||
|             objects.filter((obj): obj is FilteredLearningObject => obj !== null) | ||||
|         ); | ||||
|     } catch (error) { | ||||
|         console.error('❌ Error fetching learning objects:', error); | ||||
|         getLogger().error('❌ Error fetching learning objects:', error); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,9 +1,10 @@ | |||
| import { getAttachmentRepository } from '../../data/repositories.js'; | ||||
| import { Attachment } from '../../entities/content/attachment.entity.js'; | ||||
| import { LearningObjectIdentifier } from '../../interfaces/learning-content.js'; | ||||
| 
 | ||||
| import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| 
 | ||||
| const attachmentService = { | ||||
|     getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { | ||||
|     async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { | ||||
|         const attachmentRepo = getAttachmentRepository(); | ||||
| 
 | ||||
|         if (learningObjectId.version) { | ||||
|  |  | |||
|  | @ -1,13 +1,12 @@ | |||
| import { LearningObjectProvider } from './learning-object-provider.js'; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; | ||||
| import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js'; | ||||
| import { Language } from '../../entities/content/language.js'; | ||||
| import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||
| import { getUrlStringForLearningObject } from '../../util/links.js'; | ||||
| import processingService from './processing/processing-service.js'; | ||||
| import { NotFoundError } from '@mikro-orm/core'; | ||||
| import learningObjectService from './learning-object-service.js'; | ||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
|  | @ -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(); | ||||
| 
 | ||||
|     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> { | ||||
|         const learningObjectRepo = getLearningObjectRepository(); | ||||
| 
 | ||||
|         const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); | ||||
|         const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); | ||||
|         if (!learningObject) { | ||||
|             return null; | ||||
|         } | ||||
|         return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id)); | ||||
|         return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id)); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|  | @ -96,7 +95,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { | |||
|             throw new NotFoundError('The learning path with the given ID could not be found.'); | ||||
|         } | ||||
|         const learningObjects = await Promise.all( | ||||
|             learningPath.nodes.map((it) => { | ||||
|             learningPath.nodes.map(async (it) => { | ||||
|                 const learningObject = learningObjectService.getLearningObjectById({ | ||||
|                     hruid: it.learningObjectHruid, | ||||
|                     language: it.language, | ||||
|  |  | |||
|  | @ -1,5 +1,8 @@ | |||
| import { DWENGO_API_BASE } from '../../config.js'; | ||||
| import { fetchWithLogging } from '../../util/api-helper.js'; | ||||
| import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; | ||||
| import { LearningObjectProvider } from './learning-object-provider.js'; | ||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | ||||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectIdentifier, | ||||
|  | @ -7,10 +10,7 @@ import { | |||
|     LearningObjectNode, | ||||
|     LearningPathIdentifier, | ||||
|     LearningPathResponse, | ||||
| } from '../../interfaces/learning-content.js'; | ||||
| import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; | ||||
| import { LearningObjectProvider } from './learning-object-provider.js'; | ||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | ||||
| } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
|  | @ -66,12 +66,13 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full | |||
|         } | ||||
| 
 | ||||
|         const objects = await Promise.all( | ||||
|             nodes.map(async (node) => | ||||
|                 dwengoApiLearningObjectProvider.getLearningObjectById({ | ||||
|             nodes.map(async (node) => { | ||||
|                 const learningObjectId: LearningObjectIdentifier = { | ||||
|                     hruid: node.learningobject_hruid, | ||||
|                     language: learningPathId.language, | ||||
|                 }) | ||||
|             ) | ||||
|                 }; | ||||
|                 return dwengoApiLearningObjectProvider.getLearningObjectById(learningObjectId); | ||||
|             }) | ||||
|         ); | ||||
|         return objects.filter((obj): obj is FilteredLearningObject => obj !== null); | ||||
|     } catch (error) { | ||||
|  | @ -90,7 +91,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | |||
|             metadataUrl, | ||||
|             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, | ||||
|             { | ||||
|                 params: id, | ||||
|                 params: { ...id }, | ||||
|             } | ||||
|         ); | ||||
| 
 | ||||
|  | @ -123,7 +124,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | |||
|     async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { | ||||
|         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; | ||||
|         const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { | ||||
|             params: id, | ||||
|             params: { ...id }, | ||||
|         }); | ||||
| 
 | ||||
|         if (!html) { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| 
 | ||||
| export interface LearningObjectProvider { | ||||
|     /** | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; | ||||
| import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js'; | ||||
| import { LearningObjectProvider } from './learning-object-provider.js'; | ||||
| import { EnvVars, getEnvVar } from '../../util/envvars.js'; | ||||
| import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||
| import databaseLearningObjectProvider from './database-learning-object-provider.js'; | ||||
| import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| 
 | ||||
| function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { | ||||
|     if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { | ||||
|     if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { | ||||
|         return databaseLearningObjectProvider; | ||||
|     } | ||||
|     return dwengoApiLearningObjectProvider; | ||||
|  | @ -18,28 +18,28 @@ const learningObjectService = { | |||
|     /** | ||||
|      * Fetches a single learning object by its HRUID | ||||
|      */ | ||||
|     getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { | ||||
|     async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { | ||||
|         return getProvider(id).getLearningObjectById(id); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch full learning object data (metadata) | ||||
|      */ | ||||
|     getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { | ||||
|     async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { | ||||
|         return getProvider(id).getLearningObjectsFromPath(id); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch only learning object HRUIDs | ||||
|      */ | ||||
|     getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> { | ||||
|     async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> { | ||||
|         return getProvider(id).getLearningObjectIdsFromPath(id); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * 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); | ||||
|     }, | ||||
| }; | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ class AudioProcessor extends StringProcessor { | |||
|         super(DwengoContentType.AUDIO_MPEG); | ||||
|     } | ||||
| 
 | ||||
|     protected renderFn(audioUrl: string): string { | ||||
|     override renderFn(audioUrl: string): string { | ||||
|         return DOMPurify.sanitize(`<audio controls>
 | ||||
|             <source src="${audioUrl}" type=${type}> | ||||
|             Your browser does not support the audio element. | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ class ExternProcessor extends StringProcessor { | |||
|         super(DwengoContentType.EXTERN); | ||||
|     } | ||||
| 
 | ||||
|     override renderFn(externURL: string) { | ||||
|     override renderFn(externURL: string): string { | ||||
|         if (!isValidHttpUrl(externURL)) { | ||||
|             throw new ProcessingError('The url is not valid: ' + externURL); | ||||
|         } | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Reference in a new issue
	
	 Adriaan J.
						Adriaan J.