Merging origin/dev into feat/assignment-page correctie

This commit is contained in:
Joyelle Ndagijimana 2025-04-07 18:21:24 +02:00
commit baea0051e6
249 changed files with 6754 additions and 3612 deletions

45
.github/workflows/backend-testing.yml vendored Normal file
View file

@ -0,0 +1,45 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, run backend tests across different versions of node (here 22.x)
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Backend Testing
# Workflow runs when:
# - a backend js/ts file on "dev" changes
# - a non-draft PR to "dev" with backend js/ts files is opened, is reopened, or changes
# - a draft PR to "dev" with backend js/ts files is marked as ready for review
on:
push:
branches: [ "dev" ]
paths:
- 'backend/src/**.[jt]s'
- 'backend/tests/**.[jt]s'
- 'backend/vitest.config.ts'
pull_request:
branches: [ "dev" ]
types: ["synchronize", "ready_for_review", "opened", "reopened"]
paths:
- 'backend/src/**.[jt]s'
- 'backend/tests/**.[jt]s'
- 'backend/vitest.config.ts'
jobs:
test:
name: Run backend unit tests
if: '! github.event.pull_request.draft'
runs-on: [self-hosted, Linux, X64]
strategy:
matrix:
node-version: [22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit -w backend

View file

@ -13,7 +13,10 @@ jobs:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
-
name: Copy environment variables to correct file
run: cp /home/dev/.backend.env backend/.env
- -
name: Start docker name: Start docker
run: docker compose -f compose.yml -f compose.prod.yml up --build -d run: docker compose -f compose.production.yml up --build -d

54
.github/workflows/frontend-testing.yml vendored Normal file
View file

@ -0,0 +1,54 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, run frontend tests across different versions of node (here 22.x)
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Frontend Testing
# Workflow runs when:
# - a frontend js/ts/vue/css file on "dev" changes
# - a non-draft PR to "dev" with frontend js/ts/vue/css files is opened, is reopened, or changes
# - a draft PR to "dev" with frontend js/ts/vue/css files is marked as ready for review
on:
push:
branches: [ "dev" ]
paths:
- 'frontend/src/**.[jt]s'
- 'frontend/src/**.vue'
- 'frontend/src/**.css'
- 'frontend/tests/**.[jt]s'
- 'frontend/tests/**.vue'
- 'frontend/tests/**.css'
- 'frontend/vitest.config.ts'
- 'frontend/playwright.config.ts'
pull_request:
branches: [ "dev" ]
types: ["synchronize", "ready_for_review", "opened", "reopened"]
paths:
- 'frontend/src/**.[jt]s'
- 'frontend/src/**.vue'
- 'frontend/src/**.css'
- 'frontend/tests/**.[jt]s'
- 'frontend/tests/**.vue'
- 'frontend/tests/**.css'
- 'frontend/vitest.config.ts'
- 'frontend/playwright.config.ts'
jobs:
test:
name: Run frontend unit tests
if: '! github.event.pull_request.draft'
runs-on: [self-hosted, Linux, X64]
strategy:
matrix:
node-version: [22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit -w frontend

View file

@ -43,6 +43,6 @@ jobs:
with: with:
auto_fix: true auto_fix: true
eslint: true eslint: true
eslint_args: '--config eslint.config.ts' eslint_args: "--config eslint.config.ts --ignore-pattern '**/prettier.config.js'"
prettier: true prettier: true
commit_message: 'style: fix linting issues met ${linter}' commit_message: 'style: fix linting issues met ${linter}'

2
.gitignore vendored
View file

@ -737,4 +737,4 @@ flycheck_*.el
# network security # network security
/network-security.data /network-security.data
docs/.venv

View file

@ -35,12 +35,8 @@ Om de applicatie lokaal te draaien als kant-en-klare Docker-containers:
```bash ```bash
docker compose version docker compose version
git clone https://github.com/SELab-2/Dwengo-1.git git clone https://github.com/SELab-2/Dwengo-1.git
cd Dwengo-1/backend
cp .env.example .env
# Pas .env aan
nano .env
cd ..
docker compose -f compose.staging.yml up --build docker compose -f compose.staging.yml up --build
# Gebruikt backend/.env.staging
``` ```
### Handmatige installatie en ontwikkeling ### Handmatige installatie en ontwikkeling

21
backend/.env.staging Normal file
View 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

View file

@ -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_NAME=":memory:"
DWENGO_DB_UPDATE=true

View file

@ -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

View file

@ -1,38 +1,50 @@
FROM node:22 AS build-stage FROM node:22 AS build-stage
WORKDIR /app WORKDIR /app/dwengo
# Install dependencies # Install dependencies
COPY package*.json ./ COPY package*.json ./
COPY backend/package.json ./backend/ COPY backend/package.json ./backend/
# Backend depends on common
COPY common/package.json ./common/
RUN npm install --silent RUN npm install --silent
# Build the backend # Build the backend
# Root tsconfig.json # Root tsconfig.json
COPY tsconfig.json ./ COPY tsconfig.json tsconfig.build.json ./
WORKDIR /app/backend COPY backend ./backend
COPY common ./common
COPY backend ./ COPY docs ./docs
COPY docs /app/docs
RUN npm run build RUN npm run build
FROM node:22 AS production-stage 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 RUN npm install --silent --only=production
COPY ./docs /docs COPY ./docs ./docs
COPY ./backend/i18n /app/i18n COPY ./backend/i18n ./backend/i18n
COPY --from=build-stage /app/backend/dist ./dist/
EXPOSE 3000 EXPOSE 3000
CMD ["node", "--env-file=.env", "dist/app.js"] CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"]

View file

@ -34,7 +34,9 @@ npm run test:unit
```shell ```shell
# Omgevingsvariabelen # 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 build
npm run start npm run start

View file

@ -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;

View file

@ -8,14 +8,4 @@ export default [
globals: globals.node, globals: globals.node,
}, },
}, },
{
files: ['tests/**/*.ts'],
languageOptions: {
globals: globals.node,
},
rules: {
'no-console': 'off',
},
},
]; ];

View file

@ -1,17 +1,19 @@
{ {
"name": "dwengo-1-backend", "name": "@dwengo-1/backend",
"version": "0.1.1", "version": "0.1.1",
"description": "Backend for Dwengo-1", "description": "Backend for Dwengo-1",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "dist/app.js",
"scripts": { "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", "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", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js",
"format": "prettier --write src/", "format": "prettier --write src/",
"format-check": "prettier --check src/", "format-check": "prettier --check src/",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"test:unit": "vitest" "pretest:unit": "npm run build",
"test:unit": "vitest --run"
}, },
"dependencies": { "dependencies": {
"@mikro-orm/core": "6.4.9", "@mikro-orm/core": "6.4.9",
@ -24,6 +26,7 @@
"cross": "^1.0.0", "cross": "^1.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"dwengo-1-common": "^0.1.1",
"express": "^5.0.1", "express": "^5.0.1",
"express-jwt": "^8.5.1", "express-jwt": "^8.5.1",
"gift-pegjs": "^1.0.2", "gift-pegjs": "^1.0.2",

View file

@ -5,7 +5,7 @@ import cors from './middleware/cors.js';
import { getLogger, Logger } from './logging/initalize.js'; import { getLogger, Logger } from './logging/initalize.js';
import { responseTimeLogger } from './logging/responseTimeLogger.js'; import { responseTimeLogger } from './logging/responseTimeLogger.js';
import responseTime from 'response-time'; import responseTime from 'response-time';
import { EnvVars, getNumericEnvVar } from './util/envvars.js'; import { envVars, getNumericEnvVar } from './util/envVars.js';
import apiRouter from './routes/router.js'; import apiRouter from './routes/router.js';
import swaggerMiddleware from './swagger.js'; import swaggerMiddleware from './swagger.js';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
@ -14,7 +14,7 @@ import { errorHandler } from './middleware/error-handling/error-handler.js';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
const app: Express = express(); const app: Express = express();
const port: string | number = getNumericEnvVar(EnvVars.Port); const port: string | number = getNumericEnvVar(envVars.Port);
app.use(express.json()); app.use(express.json());
app.use(cors); app.use(cors);
@ -29,7 +29,7 @@ app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
app.use(errorHandler); app.use(errorHandler);
async function startServer() { async function startServer(): Promise<void> {
await initORM(); await initORM();
app.listen(port, () => { app.listen(port, () => {

View file

@ -1,7 +1,7 @@
import { EnvVars, getEnvVar } from './util/envvars.js'; import { envVars, getEnvVar } from './util/envVars.js';
// API // API
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl);
export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage);
export const FALLBACK_SEQ_NUM = 1; export const FALLBACK_SEQ_NUM = 1;

View file

@ -1,8 +1,8 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
import { AssignmentDTO } from '../interfaces/assignment.js'; import { AssignmentDTO } from '@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 { interface AssignmentParams {
classid: string; classid: string;
id: string; id: string;
@ -41,7 +41,7 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re
} }
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const id = +req.params.id; const id = Number(req.params.id);
const classid = req.params.classid; const classid = req.params.classid;
if (isNaN(id)) { if (isNaN(id)) {
@ -61,7 +61,7 @@ export async function getAssignmentHandler(req: Request<AssignmentParams>, res:
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid; const classid = req.params.classid;
const assignmentNumber = +req.params.id; const assignmentNumber = Number(req.params.id);
const full = req.query.full === 'true'; const full = req.query.full === 'true';
if (isNaN(assignmentNumber)) { if (isNaN(assignmentNumber)) {

View file

@ -1,16 +1,16 @@
import { EnvVars, getEnvVar } from '../util/envvars.js'; import { envVars, getEnvVar } from '../util/envVars.js';
type FrontendIdpConfig = { interface FrontendIdpConfig {
authority: string; authority: string;
clientId: string; clientId: string;
scope: string; scope: string;
responseType: string; responseType: string;
}; }
type FrontendAuthConfig = { interface FrontendAuthConfig {
student: FrontendIdpConfig; student: FrontendIdpConfig;
teacher: FrontendIdpConfig; teacher: FrontendIdpConfig;
}; }
const SCOPE = 'openid profile email'; const SCOPE = 'openid profile email';
const RESPONSE_TYPE = 'code'; const RESPONSE_TYPE = 'code';
@ -18,14 +18,14 @@ const RESPONSE_TYPE = 'code';
export function getFrontendAuthConfig(): FrontendAuthConfig { export function getFrontendAuthConfig(): FrontendAuthConfig {
return { return {
student: { student: {
authority: getEnvVar(EnvVars.IdpStudentUrl), authority: getEnvVar(envVars.IdpStudentUrl),
clientId: getEnvVar(EnvVars.IdpStudentClientId), clientId: getEnvVar(envVars.IdpStudentClientId),
scope: SCOPE, scope: SCOPE,
responseType: RESPONSE_TYPE, responseType: RESPONSE_TYPE,
}, },
teacher: { teacher: {
authority: getEnvVar(EnvVars.IdpTeacherUrl), authority: getEnvVar(envVars.IdpTeacherUrl),
clientId: getEnvVar(EnvVars.IdpTeacherClientId), clientId: getEnvVar(envVars.IdpTeacherClientId),
scope: SCOPE, scope: SCOPE,
responseType: RESPONSE_TYPE, responseType: RESPONSE_TYPE,
}, },

View file

@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js'; 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> { export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';

View 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);
}
}

View file

@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; 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 // Typescript is annoywith with parameter forwarding from class.ts
interface GroupParams { interface GroupParams {
@ -12,14 +12,14 @@ interface GroupParams {
export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> { export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> {
const classId = req.params.classid; const classId = req.params.classid;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid; const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); res.status(400).json({ error: 'Assignment id must be a number' });
return; return;
} }
const groupId = +req.params.groupid!; // Can't be undefined const groupId = Number(req.params.groupid!); // Can't be undefined
if (isNaN(groupId)) { if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' }); res.status(400).json({ error: 'Group id must be a number' });
@ -40,7 +40,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
const classId = req.params.classid; const classId = req.params.classid;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid; const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); res.status(400).json({ error: 'Assignment id must be a number' });
@ -56,7 +56,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
export async function createGroupHandler(req: Request, res: Response): Promise<void> { export async function createGroupHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid; const classid = req.params.classid;
const assignmentId = +req.params.assignmentid; const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); res.status(400).json({ error: 'Assignment id must be a number' });
@ -78,14 +78,14 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P
const classId = req.params.classid; const classId = req.params.classid;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid; const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) { if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' }); res.status(400).json({ error: 'Assignment id must be a number' });
return; return;
} }
const groupId = +req.params.groupid!; // Can't be undefined const groupId = Number(req.params.groupid); // Can't be undefined
if (isNaN(groupId)) { if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' }); res.status(400).json({ error: 'Group id must be a number' });

View file

@ -1,20 +1,20 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js';
import learningObjectService from '../services/learning-objects/learning-object-service.js'; import learningObjectService from '../services/learning-objects/learning-object-service.js';
import { EnvVars, getEnvVar } from '../util/envvars.js'; import { Language } from '@dwengo-1/common/util/language';
import { Language } from '../entities/content/language.js';
import attachmentService from '../services/learning-objects/attachment-service.js'; import attachmentService from '../services/learning-objects/attachment-service.js';
import { NotFoundError } from '@mikro-orm/core';
import { BadRequestException } from '../exceptions/bad-request-exception.js'; 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 { function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
if (!req.params.hruid) { if (!req.params.hruid) {
throw new BadRequestException('HRUID is required.'); throw new BadRequestException('HRUID is required.');
} }
return { return {
hruid: req.params.hruid as string, hruid: req.params.hruid,
language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language,
version: parseInt(req.query.version as string), version: parseInt(req.query.version as string),
}; };
} }
@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif
throw new BadRequestException('HRUID is required.'); throw new BadRequestException('HRUID is required.');
} }
return { return {
hruid: req.params.hruid as string, hruid: req.params.hruid,
language: (req.query.language as Language) || FALLBACK_LANG, language: (req.query.language as Language) || FALLBACK_LANG,
}; };
} }
@ -47,6 +47,11 @@ export async function getLearningObject(req: Request, res: Response): Promise<vo
const learningObjectId = getLearningObjectIdentifierFromRequest(req); const learningObjectId = getLearningObjectIdentifierFromRequest(req);
const learningObject = await learningObjectService.getLearningObjectById(learningObjectId); const learningObject = await learningObjectService.getLearningObjectById(learningObjectId);
if (!learningObject) {
throw new NotFoundException('Learning object not found');
}
res.json(learningObject); res.json(learningObject);
} }
@ -63,7 +68,7 @@ export async function getAttachment(req: Request, res: Response): Promise<void>
const attachment = await attachmentService.getAttachment(learningObjectId, name); const attachment = await attachmentService.getAttachment(learningObjectId, name);
if (!attachment) { 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); res.setHeader('Content-Type', attachment.mimeType).send(attachment.content);
} }

View file

@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import { themes } from '../data/themes.js'; import { themes } from '../data/themes.js';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import learningPathService from '../services/learning-paths/learning-path-service.js'; import learningPathService from '../services/learning-paths/learning-path-service.js';
import { Language } from '../entities/content/language.js'; import { Language } from '@dwengo-1/common/util/language';
import { import {
PersonalizationTarget, PersonalizationTarget,
personalizedForGroup, personalizedForGroup,

View file

@ -1,9 +1,9 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; 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 { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.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 { function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null {
const { hruid, version } = req.params; const { hruid, version } = req.params;
@ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu
return { return {
hruid, hruid,
language: (lang as Language) || FALLBACK_LANG, language: (lang as Language) || FALLBACK_LANG,
version: +version, version: Number(version),
}; };
} }

View file

@ -1,101 +1,67 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { import {
createClassJoinRequest,
createStudent, createStudent,
deleteClassJoinRequest,
deleteStudent, deleteStudent,
getAllStudents, getAllStudents,
getJoinRequestByStudentClass,
getJoinRequestsByStudent,
getStudent, getStudent,
getStudentAssignments, getStudentAssignments,
getStudentClasses, getStudentClasses,
getStudentGroups, getStudentGroups,
getStudentQuestions,
getStudentSubmissions, getStudentSubmissions,
} from '../services/students.js'; } 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> { export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const students = await getAllStudents(full); const students: StudentDTO[] | string[] = await getAllStudents(full);
if (!students) { res.json({ students });
res.status(404).json({ error: `Student not found.` });
return;
}
res.json({ students: students });
} }
export async function getStudentHandler(req: Request, res: Response): Promise<void> { export async function getStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username; const username = req.params.username;
requireFields({ username });
if (!username) { const student = await getStudent(username);
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const user = await getStudent(username); res.json({ student });
if (!user) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.json(user);
} }
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; const userData = req.body as StudentDTO;
if (!userData.username || !userData.firstName || !userData.lastName) { const student = await createStudent(userData);
res.status(400).json({ res.json({ student });
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);
} }
export async function deleteStudentHandler(req: Request, res: Response) { export async function deleteStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username; const username = req.params.username;
requireFields({ username });
if (!username) { const student = await deleteStudent(username);
res.status(400).json({ error: 'Missing required field: username' }); res.json({ student });
return;
}
const deletedUser = await deleteStudent(username);
if (!deletedUser) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
} }
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> { export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const username = req.params.id; const username = req.params.username;
requireFields({ username });
const classes = await getStudentClasses(username, full); const classes = await getStudentClasses(username, full);
res.json({ res.json({ classes });
classes: classes,
});
} }
// TODO // TODO
@ -104,33 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro
// Have this assignment. // Have this assignment.
export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> { export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const username = req.params.id; const username = req.params.username;
requireFields({ username });
const assignments = getStudentAssignments(username, full); const assignments = getStudentAssignments(username, full);
res.json({ res.json({ assignments });
assignments: assignments,
});
} }
export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> { export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const username = req.params.id; const username = req.params.username;
requireFields({ username });
const groups = await getStudentGroups(username, full); const groups = await getStudentGroups(username, full);
res.json({ res.json({ groups });
groups: groups,
});
} }
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> { 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'; const full = req.query.full === 'true';
requireFields({ username });
const submissions = await getStudentSubmissions(username, full); const submissions = await getStudentSubmissions(username, full);
res.json({ res.json({ submissions });
submissions: 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 });
} }

View file

@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js';
import { Language, languageMap } from '../entities/content/language.js'; import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
import { SubmissionDTO } from '../interfaces/submission'; import { Language, languageMap } from '@dwengo-1/common/util/language';
interface SubmissionParams { interface SubmissionParams {
hruid: string; hruid: string;
@ -10,7 +10,7 @@ interface SubmissionParams {
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> { export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> {
const lohruid = req.params.hruid; const lohruid = req.params.hruid;
const submissionNumber = +req.params.id; const submissionNumber = Number(req.params.id);
if (isNaN(submissionNumber)) { if (isNaN(submissionNumber)) {
res.status(400).json({ error: 'Submission number is not a number' }); res.status(400).json({ error: 'Submission number is not a number' });
@ -30,7 +30,7 @@ export async function getSubmissionHandler(req: Request<SubmissionParams>, res:
res.json(submission); res.json(submission);
} }
export async function createSubmissionHandler(req: Request, res: Response) { export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
const submissionDTO = req.body as SubmissionDTO; const submissionDTO = req.body as SubmissionDTO;
const submission = await createSubmission(submissionDTO); const submission = await createSubmission(submissionDTO);
@ -43,9 +43,9 @@ export async function createSubmissionHandler(req: Request, res: Response) {
res.json(submission); res.json(submission);
} }
export async function deleteSubmissionHandler(req: Request, res: Response) { export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid; const hruid = req.params.hruid;
const submissionNumber = +req.params.id; const submissionNumber = Number(req.params.id);
const lang = languageMap[req.query.language as string] || Language.Dutch; const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number; const version = (req.query.version || 1) as number;

View file

@ -4,137 +4,97 @@ import {
deleteTeacher, deleteTeacher,
getAllTeachers, getAllTeachers,
getClassesByTeacher, getClassesByTeacher,
getQuestionsByTeacher, getJoinRequestsByClass,
getStudentsByTeacher, getStudentsByTeacher,
getTeacher, getTeacher,
getTeacherQuestions,
updateClassJoinRequestStatus,
} from '../services/teachers.js'; } 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> { export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const teachers = await getAllTeachers(full); const teachers: TeacherDTO[] | string[] = await getAllTeachers(full);
if (!teachers) { res.json({ teachers });
res.status(404).json({ error: `Teacher not found.` });
return;
}
res.json({ teachers: teachers });
} }
export async function getTeacherHandler(req: Request, res: Response): Promise<void> { export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username; const username = req.params.username;
requireFields({ username });
if (!username) { const teacher = await getTeacher(username);
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const user = await getTeacher(username); res.json({ teacher });
if (!user) {
res.status(404).json({
error: `Teacher '${username}' not found.`,
});
return;
}
res.json(user);
} }
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; const userData = req.body as TeacherDTO;
if (!userData.username || !userData.firstName || !userData.lastName) { const teacher = await createTeacher(userData);
res.status(400).json({ res.json({ teacher });
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);
} }
export async function deleteTeacherHandler(req: Request, res: Response) { export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username; const username = req.params.username;
requireFields({ username });
if (!username) { const teacher = await deleteTeacher(username);
res.status(400).json({ error: 'Missing required field: username' }); res.json({ teacher });
return;
}
const deletedUser = await deleteTeacher(username);
if (!deletedUser) {
res.status(404).json({
error: `User '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
} }
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username as string; const username = req.params.username;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ username });
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const classes = await getClassesByTeacher(username, full); const classes = await getClassesByTeacher(username, full);
if (!classes) { res.json({ classes });
res.status(404).json({ error: 'Teacher not found' });
return;
}
res.json({ classes: classes });
} }
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username as string; const username = req.params.username;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ username });
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const students = await getStudentsByTeacher(username, full); const students = await getStudentsByTeacher(username, full);
if (!students) { res.json({ students });
res.status(404).json({ error: 'Teacher not found' });
return;
}
res.json({ students: students });
} }
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username as string; const username = req.params.username;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
requireFields({ username });
if (!username) { const questions = await getTeacherQuestions(username, full);
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const questions = await getQuestionsByTeacher(username, full); res.json({ questions });
}
if (!questions) {
res.status(404).json({ error: 'Teacher not found' }); export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
return; const username = req.query.username as string;
} const classId = req.params.classId;
requireFields({ username, classId });
res.json({ questions: questions });
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 });
} }

View file

@ -3,25 +3,23 @@ import { themes } from '../data/themes.js';
import { loadTranslations } from '../util/translation-helper.js'; import { loadTranslations } from '../util/translation-helper.js';
interface Translations { interface Translations {
curricula_page: { curricula_page: Record<string, { title: string; description?: string }>;
[key: string]: { title: string; description?: string };
};
} }
export function getThemesHandler(req: Request, res: Response) { export function getThemesHandler(req: Request, res: Response): void {
const language = (req.query.language as string)?.toLowerCase() || 'nl'; const language = ((req.query.language as string) || 'nl').toLowerCase();
const translations = loadTranslations<Translations>(language); const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => ({ const themeList = themes.map((theme) => ({
key: theme.title, key: theme.title,
title: translations.curricula_page[theme.title]?.title || theme.title, title: translations.curricula_page[theme.title].title || theme.title,
description: translations.curricula_page[theme.title]?.description, description: translations.curricula_page[theme.title].description,
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
})); }));
res.json(themeList); res.json(themeList);
} }
export function getHruidsByThemeHandler(req: Request, res: Response) { export function getHruidsByThemeHandler(req: Request, res: Response): void {
const themeKey = req.params.theme; const themeKey = req.params.theme;
if (!themeKey) { if (!themeKey) {

View file

@ -3,13 +3,13 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Class } from '../../entities/classes/class.entity.js'; import { Class } from '../../entities/classes/class.entity.js';
export class AssignmentRepository extends DwengoEntityRepository<Assignment> { export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
public findByClassAndId(within: Class, id: number): Promise<Assignment | null> { public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
return this.findOne({ within: within, id: id }); return this.findOne({ within: within, id: id });
} }
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
return this.findAll({ where: { within: within } }); return this.findAll({ where: { within: within } });
} }
public deleteByClassAndId(within: Class, id: number): Promise<void> { public async deleteByClassAndId(within: Class, id: number): Promise<void> {
return this.deleteWhere({ within: within, id: id }); return this.deleteWhere({ within: within, id: id });
} }
} }

View file

@ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
export class GroupRepository extends DwengoEntityRepository<Group> { export class GroupRepository extends DwengoEntityRepository<Group> {
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
return this.findOne( return this.findOne(
{ {
assignment: assignment, assignment: assignment,
@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> {
{ populate: ['members'] } { populate: ['members'] }
); );
} }
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
return this.findAll({ return this.findAll({
where: { assignment: assignment }, where: { assignment: assignment },
populate: ['members'], populate: ['members'],
}); });
} }
public findAllGroupsWithStudent(student: Student): Promise<Group[]> { public async findAllGroupsWithStudent(student: Student): Promise<Group[]> {
return this.find({ members: student }, { populate: ['members'] }); return this.find({ members: student }, { populate: ['members'] });
} }
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
assignment: assignment, assignment: assignment,
groupNumber: groupNumber, groupNumber: groupNumber,

View file

@ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
export class SubmissionRepository extends DwengoEntityRepository<Submission> { export class SubmissionRepository extends DwengoEntityRepository<Submission> {
public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> { public async findSubmissionByLearningObjectAndSubmissionNumber(
loId: LearningObjectIdentifier,
submissionNumber: number
): Promise<Submission | null> {
return this.findOne({ return this.findOne({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,
@ -14,7 +17,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
}); });
} }
public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> { public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
return this.findOne( return this.findOne(
{ {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -26,7 +29,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
); );
} }
public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> { public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
return this.findOne( return this.findOne(
{ {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -38,15 +41,15 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
); );
} }
public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> { public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
return this.find({ onBehalfOf: group }); return this.find({ onBehalfOf: group });
} }
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
return this.find({ submitter: student }); return this.find({ submitter: student });
} }
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> { public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,

View file

@ -2,15 +2,19 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Class } from '../../entities/classes/class.entity.js'; import { Class } from '../../entities/classes/class.entity.js';
import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js';
import { Student } from '../../entities/users/student.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> { export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> { public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { requester: requester } }); return this.findAll({ where: { requester: requester } });
} }
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { class: clazz } }); return this.findAll({ where: { class: clazz, 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 }); return this.deleteWhere({ requester: requester, class: clazz });
} }
} }

View file

@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js';
import { Teacher } from '../../entities/users/teacher.entity'; import { Teacher } from '../../entities/users/teacher.entity';
export class ClassRepository extends DwengoEntityRepository<Class> { export class ClassRepository extends DwengoEntityRepository<Class> {
public findById(id: string): Promise<Class | null> { public async findById(id: string): Promise<Class | null> {
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
} }
public deleteById(id: string): Promise<void> { public async deleteById(id: string): Promise<void> {
return this.deleteWhere({ classId: id }); return this.deleteWhere({ classId: id });
} }
public findByStudent(student: Student): Promise<Class[]> { public async findByStudent(student: Student): Promise<Class[]> {
return this.find( return this.find(
{ students: student }, { students: student },
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
); );
} }
public findByTeacher(teacher: Teacher): Promise<Class[]> { public async findByTeacher(teacher: Teacher): Promise<Class[]> {
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
} }
} }

View file

@ -4,16 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> { export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> { public async findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
return this.findAll({ where: { class: clazz } }); return this.findAll({ where: { class: clazz } });
} }
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> { public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
return this.findAll({ where: { sender: sender } }); return this.findAll({ where: { sender: sender } });
} }
public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> { public async findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
return this.findAll({ where: { receiver: receiver } }); return this.findAll({ where: { receiver: receiver } });
} }
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> { public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
sender: sender, sender: sender,
receiver: receiver, receiver: receiver,

View file

@ -1,10 +1,10 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { Attachment } from '../../entities/content/attachment.entity.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'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
export class AttachmentRepository extends DwengoEntityRepository<Attachment> { export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> { public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
return this.findOne({ return this.findOne({
learningObject: { learningObject: {
hruid: learningObjectId.hruid, hruid: learningObjectId.hruid,
@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
}); });
} }
public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise<Attachment | null> { public async findByMostRecentVersionOfLearningObjectAndName(
hruid: string,
language: Language,
attachmentName: string
): Promise<Attachment | null> {
return this.findOne( return this.findOne(
{ {
learningObject: { learningObject: {

View file

@ -1,11 +1,11 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.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'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
return this.findOne( return this.findOne(
{ {
hruid: identifier.hruid, hruid: identifier.hruid,
@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
); );
} }
public findLatestByHruidAndLanguage(hruid: string, language: Language) { public async findLatestByHruidAndLanguage(hruid: string, language: Language): Promise<LearningObject | null> {
return this.findOne( return this.findOne(
{ {
hruid: hruid, hruid: hruid,
@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
); );
} }
public findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> { public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
return this.find( return this.find(
{ admins: teacher }, { admins: teacher },
{ populate: ['admins'] } // Make sure to load admin relations { populate: ['admins'] } // Make sure to load admin relations

View file

@ -1,9 +1,9 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningPath } from '../../entities/content/learning-path.entity.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> { export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
} }

View file

@ -8,7 +8,7 @@ export abstract class DwengoEntityRepository<T extends object> extends EntityRep
} }
await this.getEntityManager().persistAndFlush(entity); await this.getEntityManager().persistAndFlush(entity);
} }
public async deleteWhere(query: FilterQuery<T>) { public async deleteWhere(query: FilterQuery<T>): Promise<void> {
const toDelete = await this.findOne(query); const toDelete = await this.findOne(query);
const em = this.getEntityManager(); const em = this.getEntityManager();
if (toDelete) { if (toDelete) {

View file

@ -4,7 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js';
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class AnswerRepository extends DwengoEntityRepository<Answer> { export class AnswerRepository extends DwengoEntityRepository<Answer> {
public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
const answerEntity = this.create({ const answerEntity = this.create({
toQuestion: answer.toQuestion, toQuestion: answer.toQuestion,
author: answer.author, author: answer.author,
@ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
}); });
return this.insert(answerEntity); return this.insert(answerEntity);
} }
public findAllAnswersToQuestion(question: Question): Promise<Answer[]> { public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
return this.findAll({ return this.findAll({
where: { toQuestion: question }, where: { toQuestion: question },
orderBy: { sequenceNumber: 'ASC' }, orderBy: { sequenceNumber: 'ASC' },
}); });
} }
public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
toQuestion: question, toQuestion: question,
sequenceNumber: sequenceNumber, sequenceNumber: sequenceNumber,

View file

@ -5,7 +5,7 @@ import { Student } from '../../entities/users/student.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
export class QuestionRepository extends DwengoEntityRepository<Question> { export class QuestionRepository extends DwengoEntityRepository<Question> {
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
const questionEntity = this.create({ const questionEntity = this.create({
learningObjectHruid: question.loId.hruid, learningObjectHruid: question.loId.hruid,
learningObjectLanguage: question.loId.language, learningObjectLanguage: question.loId.language,
@ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
questionEntity.content = question.content; questionEntity.content = question.content;
return this.insert(questionEntity); return this.insert(questionEntity);
} }
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
return this.findAll({ return this.findAll({
where: { where: {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -33,7 +33,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
}, },
}); });
} }
public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> { public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,
@ -54,4 +54,11 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
orderBy: { timestamp: 'ASC' }, orderBy: { timestamp: 'ASC' },
}); });
} }
public async findAllByAuthor(author: Student): Promise<Question[]> {
return this.findAll({
where: { author },
orderBy: { timestamp: 'DESC' }, // New to old
});
}
} }

View file

@ -34,8 +34,8 @@ let entityManager: EntityManager | undefined;
/** /**
* Execute all the database operations within the function f in a single transaction. * Execute all the database operations within the function f in a single transaction.
*/ */
export function transactional<T>(f: () => Promise<T>) { export async function transactional<T>(f: () => Promise<T>): Promise<void> {
entityManager?.transactional(f); await entityManager?.transactional(f);
} }
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R { function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {

View file

@ -1,7 +1,4 @@
export interface Theme { import { Theme } from '@dwengo-1/common/interfaces/theme';
title: string;
hruids: string[];
}
export const themes: Theme[] = [ export const themes: Theme[] = [
{ {

View file

@ -1,14 +1,11 @@
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
// Import { UserRepository } from './user-repository.js';
// Export class StudentRepository extends UserRepository<Student> {}
export class StudentRepository extends DwengoEntityRepository<Student> { export class StudentRepository extends DwengoEntityRepository<Student> {
public findByUsername(username: string): Promise<Student | null> { public async findByUsername(username: string): Promise<Student | null> {
return this.findOne({ username: username }); return this.findOne({ username: username });
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username }); return this.deleteWhere({ username: username });
} }
} }

View file

@ -2,10 +2,10 @@ import { Teacher } from '../../entities/users/teacher.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
export class TeacherRepository extends DwengoEntityRepository<Teacher> { export class TeacherRepository extends DwengoEntityRepository<Teacher> {
public findByUsername(username: string): Promise<Teacher | null> { public async findByUsername(username: string): Promise<Teacher | null> {
return this.findOne({ username: username }); return this.findOne({ username: username });
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username: username }); return this.deleteWhere({ username: username });
} }
} }

View file

@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { User } from '../../entities/users/user.entity.js'; import { User } from '../../entities/users/user.entity.js';
export class UserRepository<T extends User> extends DwengoEntityRepository<T> { export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
public findByUsername(username: string): Promise<T | null> { public async findByUsername(username: string): Promise<T | null> {
return this.findOne({ username } as Partial<T>); return this.findOne({ username } as Partial<T>);
} }
public deleteByUsername(username: string): Promise<void> { public async deleteByUsername(username: string): Promise<void> {
return this.deleteWhere({ username } as Partial<T>); return this.deleteWhere({ username } as Partial<T>);
} }
} }

View file

@ -1,7 +1,7 @@
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Class } from '../classes/class.entity.js'; import { Class } from '../classes/class.entity.js';
import { Group } from './group.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'; import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
@Entity({ @Entity({

View file

@ -1,8 +1,8 @@
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { Group } from './group.entity.js'; import { Group } from './group.entity.js';
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; 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 { SubmissionRepository } from '../../data/assignments/submission-repository.js';
import { Language } from '@dwengo-1/common/util/language';
@Entity({ repository: () => SubmissionRepository }) @Entity({ repository: () => SubmissionRepository })
export class Submission { export class Submission {
@ -16,7 +16,7 @@ export class Submission {
learningObjectLanguage!: Language; learningObjectLanguage!: Language;
@PrimaryKey({ type: 'numeric' }) @PrimaryKey({ type: 'numeric' })
learningObjectVersion: number = 1; learningObjectVersion = 1;
@PrimaryKey({ type: 'integer', autoincrement: true }) @PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber?: number; submissionNumber?: number;

View file

@ -2,12 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core';
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { Class } from './class.entity.js'; import { Class } from './class.entity.js';
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request';
export enum ClassJoinRequestStatus {
Open = 'open',
Accepted = 'accepted',
Declined = 'declined',
}
@Entity({ @Entity({
repository: () => ClassJoinRequestRepository, repository: () => ClassJoinRequestRepository,

View 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;
}

View file

@ -1,9 +1,11 @@
import { Language } from './language.js'; import { Language } from '@dwengo-1/common/util/language';
export class LearningObjectIdentifier { export class LearningObjectIdentifier {
constructor( constructor(
public hruid: string, public hruid: string,
public language: Language, public language: Language,
public version: number public version: number
) {} ) {
// Do nothing
}
} }

View file

@ -1,28 +1,12 @@
import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from './language.js';
import { Attachment } from './attachment.entity.js'; import { Attachment } from './attachment.entity.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { LearningObjectRepository } from '../../data/content/learning-object-repository.js'; import { LearningObjectRepository } from '../../data/content/learning-object-repository.js';
import { EducationalGoal } from './educational-goal.entity.js';
@Embeddable() import { ReturnValue } from './return-value.entity.js';
export class EducationalGoal { import { Language } from '@dwengo-1/common/util/language';
@Property({ type: 'string' })
source!: string;
@Property({ type: 'string' })
id!: string;
}
@Embeddable()
export class ReturnValue {
@Property({ type: 'string' })
callbackUrl!: string;
@Property({ type: 'json' })
callbackSchema!: string;
}
@Entity({ repository: () => LearningObjectRepository }) @Entity({ repository: () => LearningObjectRepository })
export class LearningObject { export class LearningObject {
@ -36,7 +20,7 @@ export class LearningObject {
language!: Language; language!: Language;
@PrimaryKey({ type: 'number' }) @PrimaryKey({ type: 'number' })
version: number = 1; version = 1;
@Property({ type: 'uuid', unique: true }) @Property({ type: 'uuid', unique: true })
uuid = v4(); uuid = v4();
@ -62,7 +46,7 @@ export class LearningObject {
targetAges?: number[] = []; targetAges?: number[] = [];
@Property({ type: 'bool' }) @Property({ type: 'bool' })
teacherExclusive: boolean = false; teacherExclusive = false;
@Property({ type: 'array' }) @Property({ type: 'array' })
skosConcepts: string[] = []; skosConcepts: string[] = [];
@ -74,10 +58,10 @@ export class LearningObject {
educationalGoals: EducationalGoal[] = []; educationalGoals: EducationalGoal[] = [];
@Property({ type: 'string' }) @Property({ type: 'string' })
copyright: string = ''; copyright = '';
@Property({ type: 'string' }) @Property({ type: 'string' })
license: string = ''; license = '';
@Property({ type: 'smallint', nullable: true }) @Property({ type: 'smallint', nullable: true })
difficulty?: number; difficulty?: number;
@ -91,7 +75,7 @@ export class LearningObject {
returnValue!: ReturnValue; returnValue!: ReturnValue;
@Property({ type: 'bool' }) @Property({ type: 'bool' })
available: boolean = true; available = true;
@Property({ type: 'string', nullable: true }) @Property({ type: 'string', nullable: true })
contentLocation?: string; contentLocation?: string;

View file

@ -1,7 +1,7 @@
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; 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 { LearningPath } from './learning-path.entity.js';
import { LearningPathTransition } from './learning-path-transition.entity.js'; import { LearningPathTransition } from './learning-path-transition.entity.js';
import { Language } from '@dwengo-1/common/util/language';
@Entity() @Entity()
export class LearningPathNode { export class LearningPathNode {

View file

@ -1,8 +1,8 @@
import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from './language.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
import { LearningPathNode } from './learning-path-node.entity.js'; import { LearningPathNode } from './learning-path-node.entity.js';
import { Language } from '@dwengo-1/common/util/language';
@Entity({ repository: () => LearningPathRepository }) @Entity({ repository: () => LearningPathRepository })
export class LearningPath { export class LearningPath {

View 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;
}

View file

@ -1,7 +1,7 @@
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from '../content/language.js';
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { QuestionRepository } from '../../data/questions/question-repository.js'; import { QuestionRepository } from '../../data/questions/question-repository.js';
import { Language } from '@dwengo-1/common/util/language';
@Entity({ repository: () => QuestionRepository }) @Entity({ repository: () => QuestionRepository })
export class Question { export class Question {
@ -15,7 +15,7 @@ export class Question {
learningObjectLanguage!: Language; learningObjectLanguage!: Language;
@PrimaryKey({ type: 'number' }) @PrimaryKey({ type: 'number' })
learningObjectVersion: number = 1; learningObjectVersion = 1;
@PrimaryKey({ type: 'integer', autoincrement: true }) @PrimaryKey({ type: 'integer', autoincrement: true })
sequenceNumber?: number; sequenceNumber?: number;

View file

@ -6,8 +6,8 @@ export abstract class User {
username!: string; username!: string;
@Property() @Property()
firstName: string = ''; firstName = '';
@Property() @Property()
lastName: string = ''; lastName = '';
} }

View file

@ -6,7 +6,7 @@ import { ExceptionWithHttpState } from './exception-with-http-state.js';
export class ForbiddenException extends ExceptionWithHttpState { export class ForbiddenException extends ExceptionWithHttpState {
status = 403; status = 403;
constructor(message: string = 'Forbidden') { constructor(message = 'Forbidden') {
super(403, message); super(403, message);
} }
} }

View file

@ -4,7 +4,7 @@ import { ExceptionWithHttpState } from './exception-with-http-state.js';
* Exception for HTTP 401 Unauthorized * Exception for HTTP 401 Unauthorized
*/ */
export class UnauthorizedException extends ExceptionWithHttpState { export class UnauthorizedException extends ExceptionWithHttpState {
constructor(message: string = 'Unauthorized') { constructor(message = 'Unauthorized') {
super(401, message); super(401, message);
} }
} }

View file

@ -1,14 +1,7 @@
import { mapToUserDTO, UserDTO } from './user.js'; import { mapToUserDTO } from './user.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js'; import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js';
import { Answer } from '../entities/questions/answer.entity.js'; import { Answer } from '../entities/questions/answer.entity.js';
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
export interface AnswerDTO {
author: UserDTO;
toQuestion: QuestionDTO;
sequenceNumber: number;
timestamp: string;
content: string;
}
/** /**
* Convert a Question entity to a DTO format. * Convert a Question entity to a DTO format.
@ -23,16 +16,10 @@ export function mapToAnswerDTO(answer: Answer): AnswerDTO {
}; };
} }
export interface AnswerId { export function mapToAnswerDTOId(answer: Answer): AnswerId {
author: string;
toQuestion: QuestionId;
sequenceNumber: number;
}
export function mapToAnswerId(answer: AnswerDTO): AnswerId {
return { return {
author: answer.author.username, author: answer.author.username,
toQuestion: mapToQuestionId(answer.toQuestion), toQuestion: mapToQuestionDTOId(answer.toQuestion),
sequenceNumber: answer.sequenceNumber, sequenceNumber: answer.sequenceNumber!,
}; };
} }

View file

@ -1,18 +1,9 @@
import { languageMap } from '@dwengo-1/common/util/language';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import { Assignment } from '../entities/assignments/assignment.entity.js'; import { Assignment } from '../entities/assignments/assignment.entity.js';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { languageMap } from '../entities/content/language.js'; import { getLogger } from '../logging/initalize.js';
import { GroupDTO } from './group.js'; import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
export interface AssignmentDTO {
id: number;
class: string; // Id of class 'within'
title: string;
description: string;
learningPath: string;
language: string;
groups?: GroupDTO[] | string[]; // TODO
}
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO {
return { return {
@ -46,5 +37,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
assignment.within = cls; assignment.within = cls;
getLogger().debug(assignment);
return assignment; return assignment;
} }

View file

@ -2,14 +2,7 @@ import { Collection } from '@mikro-orm/core';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { Student } from '../entities/users/student.entity.js'; import { Student } from '../entities/users/student.entity.js';
import { Teacher } from '../entities/users/teacher.entity.js'; import { Teacher } from '../entities/users/teacher.entity.js';
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
export interface ClassDTO {
id: string;
displayName: string;
teachers: string[];
students: string[];
joinRequests: string[];
}
export function mapToClassDTO(cls: Class): ClassDTO { export function mapToClassDTO(cls: Class): ClassDTO {
return { return {

View file

@ -1,12 +1,7 @@
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js'; import { mapToAssignmentDTO } from './assignment.js';
import { mapToStudentDTO, StudentDTO } from './student.js'; import { mapToStudentDTO } from './student.js';
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
export interface GroupDTO {
assignment: number | AssignmentDTO;
groupNumber: number;
members: string[] | StudentDTO[];
}
export function mapToGroupDTO(group: Group): GroupDTO { export function mapToGroupDTO(group: Group): GroupDTO {
return { return {

View file

@ -1,24 +1,21 @@
import { Question } from '../entities/questions/question.entity.js'; import { Question } from '../entities/questions/question.entity.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { mapToStudentDTO } from './student.js';
import { mapToStudentDTO, StudentDTO } from './student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content';
export interface QuestionDTO { function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier {
learningObjectIdentifier: LearningObjectIdentifier; return {
sequenceNumber?: number; hruid: question.learningObjectHruid,
author: StudentDTO; language: question.learningObjectLanguage,
timestamp?: string; version: question.learningObjectVersion,
content: string; };
} }
/** /**
* Convert a Question entity to a DTO format. * Convert a Question entity to a DTO format.
*/ */
export function mapToQuestionDTO(question: Question): QuestionDTO { export function mapToQuestionDTO(question: Question): QuestionDTO {
const learningObjectIdentifier = { const learningObjectIdentifier = getLearningObjectIdentifier(question);
hruid: question.learningObjectHruid,
language: question.learningObjectLanguage,
version: question.learningObjectVersion,
};
return { return {
learningObjectIdentifier, learningObjectIdentifier,
@ -29,14 +26,11 @@ export function mapToQuestionDTO(question: Question): QuestionDTO {
}; };
} }
export interface QuestionId { export function mapToQuestionDTOId(question: Question): QuestionId {
learningObjectIdentifier: LearningObjectIdentifier; const learningObjectIdentifier = getLearningObjectIdentifier(question);
sequenceNumber: number;
}
export function mapToQuestionId(question: QuestionDTO): QuestionId {
return { return {
learningObjectIdentifier: question.learningObjectIdentifier, learningObjectIdentifier,
sequenceNumber: question.sequenceNumber!, sequenceNumber: question.sequenceNumber!,
}; };
} }

View 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,
});
}

View file

@ -1,18 +1,6 @@
import { Student } from '../entities/users/student.entity.js'; import { Student } from '../entities/users/student.entity.js';
import { getStudentRepository } from '../data/repositories.js'; import { getStudentRepository } from '../data/repositories.js';
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
export interface StudentDTO {
id: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
classes: string;
questions: string;
invitations: string;
groups: string;
};
}
export function mapToStudentDTO(student: Student): StudentDTO { export function mapToStudentDTO(student: Student): StudentDTO {
return { return {

View file

@ -1,26 +1,7 @@
import { Submission } from '../entities/assignments/submission.entity.js'; import { Submission } from '../entities/assignments/submission.entity.js';
import { Language } from '../entities/content/language.js'; import { mapToGroupDTO } from './group.js';
import { GroupDTO, mapToGroupDTO } from './group.js'; import { mapToStudent, mapToStudentDTO } from './student.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
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;
}
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
return { return {

View file

@ -1,12 +1,7 @@
import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js';
import { ClassDTO, mapToClassDTO } from './class.js'; import { mapToClassDTO } from './class.js';
import { mapToUserDTO, UserDTO } from './user.js'; import { mapToUserDTO } from './user.js';
import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation';
export interface TeacherInvitationDTO {
sender: string | UserDTO;
receiver: string | UserDTO;
class: string | ClassDTO;
}
export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO {
return { return {

View file

@ -1,18 +1,6 @@
import { Teacher } from '../entities/users/teacher.entity.js'; import { Teacher } from '../entities/users/teacher.entity.js';
import { getTeacherRepository } from '../data/repositories.js'; import { getTeacherRepository } from '../data/repositories.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
export interface TeacherDTO {
id: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
classes: string;
questions: string;
invitations: string;
groups: string;
};
}
export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
return { return {

View file

@ -1,17 +1,5 @@
import { User } from '../entities/users/user.entity.js'; import { User } from '../entities/users/user.entity.js';
import { UserDTO } from '@dwengo-1/common/interfaces/user';
export interface UserDTO {
id?: string;
username: string;
firstName: string;
lastName: string;
endpoints?: {
self: string;
classes: string;
questions: string;
invitations: string;
};
}
export function mapToUserDTO(user: User): UserDTO { export function mapToUserDTO(user: User): UserDTO {
return { return {

View file

@ -1,7 +1,7 @@
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
import LokiTransport from 'winston-loki'; import LokiTransport from 'winston-loki';
import { LokiLabels } from 'loki-logger-ts'; import { LokiLabels } from 'loki-logger-ts';
import { EnvVars, getEnvVar } from '../util/envvars.js'; import { envVars, getEnvVar } from '../util/envVars.js';
export class Logger extends WinstonLogger { export class Logger extends WinstonLogger {
constructor() { constructor() {
@ -9,7 +9,7 @@ export class Logger extends WinstonLogger {
} }
} }
const Labels: LokiLabels = { const lokiLabels: LokiLabels = {
source: 'Dwengo-Backend', source: 'Dwengo-Backend',
service: 'API', service: 'API',
host: 'localhost', host: 'localhost',
@ -22,28 +22,28 @@ function initializeLogger(): Logger {
return logger; return logger;
} }
const logLevel = getEnvVar(EnvVars.LogLevel); const logLevel = getEnvVar(envVars.LogLevel);
const consoleTransport = new transports.Console({ const consoleTransport = new transports.Console({
level: getEnvVar(EnvVars.LogLevel), level: getEnvVar(envVars.LogLevel),
format: format.combine(format.cli(), format.colorize()), format: format.combine(format.cli(), format.colorize()),
}); });
if (getEnvVar(EnvVars.RunMode) === 'dev') { if (getEnvVar(envVars.RunMode) === 'dev') {
return createLogger({ return createLogger({
transports: [consoleTransport], transports: [consoleTransport],
}); });
} }
const lokiHost = getEnvVar(EnvVars.LokiHost); const lokiHost = getEnvVar(envVars.LokiHost);
const lokiTransport: LokiTransport = new LokiTransport({ const lokiTransport: LokiTransport = new LokiTransport({
host: lokiHost, host: lokiHost,
labels: Labels, labels: lokiLabels,
level: logLevel, level: logLevel,
json: true, json: true,
format: format.combine(format.timestamp(), format.json()), format: format.combine(format.timestamp(), format.json()),
onConnectionError: (err) => { onConnectionError: (err): void => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`Connection error: ${err}`); console.error(`Connection error: ${err}`);
}, },

View file

@ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts';
export class MikroOrmLogger extends DefaultLogger { export class MikroOrmLogger extends DefaultLogger {
private logger: Logger = getLogger(); private logger: Logger = getLogger();
log(namespace: LoggerNamespace, message: string, context?: LogContext) { static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown {
const labels: LokiLabels = {
service: 'ORM',
};
let message: string;
if (context?.label) {
message = `[${namespace}] (${context.label}) ${messageArg}`;
} else {
message = `[${namespace}] ${messageArg}`;
}
return {
message: message,
labels: labels,
context: context,
};
}
log(namespace: LoggerNamespace, message: string, context?: LogContext): void {
if (!this.isEnabled(namespace, context)) { if (!this.isEnabled(namespace, context)) {
return; return;
} }
switch (namespace) { switch (namespace) {
case 'query': case 'query':
this.logger.debug(this.createMessage(namespace, message, context)); this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'query-params': case 'query-params':
// TODO Which log level should this be? // TODO Which log level should this be?
this.logger.info(this.createMessage(namespace, message, context)); this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'schema': case 'schema':
this.logger.info(this.createMessage(namespace, message, context)); this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'discovery': case 'discovery':
this.logger.debug(this.createMessage(namespace, message, context)); this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'info': case 'info':
this.logger.info(this.createMessage(namespace, message, context)); this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'deprecated': case 'deprecated':
this.logger.warn(this.createMessage(namespace, message, context)); this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
default: default:
switch (context?.level) { switch (context?.level) {
case 'info': case 'info':
this.logger.info(this.createMessage(namespace, message, context)); this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
break; break;
case 'warning': case 'warning':
this.logger.warn(message); this.logger.warn(message);
@ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger {
} }
} }
} }
private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) {
const labels: LokiLabels = {
service: 'ORM',
};
let message: string;
if (context?.label) {
message = `[${namespace}] (${context?.label}) ${messageArg}`;
} else {
message = `[${namespace}] ${messageArg}`;
}
return {
message: message,
labels: labels,
context: context,
};
}
} }

View file

@ -1,7 +1,7 @@
import { getLogger, Logger } from './initalize.js'; import { getLogger, Logger } from './initalize.js';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
export function responseTimeLogger(req: Request, res: Response, time: number) { export function responseTimeLogger(req: Request, res: Response, time: number): void {
const logger: Logger = getLogger(); const logger: Logger = getLogger();
const method = req.method; const method = req.method;

View file

@ -1,9 +1,9 @@
import { EnvVars, getEnvVar } from '../../util/envvars.js'; import { envVars, getEnvVar } from '../../util/envVars.js';
import { expressjwt } from 'express-jwt'; import { expressjwt } from 'express-jwt';
import * as jwt from 'jsonwebtoken';
import { JwtPayload } from 'jsonwebtoken'; import { JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa'; import jwksClient from 'jwks-rsa';
import * as express from 'express'; import * as express from 'express';
import * as jwt from 'jsonwebtoken';
import { AuthenticatedRequest } from './authenticated-request.js'; import { AuthenticatedRequest } from './authenticated-request.js';
import { AuthenticationInfo } from './authentication-info.js'; import { AuthenticationInfo } from './authentication-info.js';
import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
@ -33,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient {
const idpConfigs = { const idpConfigs = {
student: { student: {
issuer: getEnvVar(EnvVars.IdpStudentUrl), issuer: getEnvVar(envVars.IdpStudentUrl),
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)),
}, },
teacher: { teacher: {
issuer: getEnvVar(EnvVars.IdpTeacherUrl), issuer: getEnvVar(envVars.IdpTeacherUrl),
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)),
}, },
}; };
@ -64,7 +64,7 @@ const verifyJwtToken = expressjwt({
} }
return signingKey.getPublicKey(); return signingKey.getPublicKey();
}, },
audience: getEnvVar(EnvVars.IdpAudience), audience: getEnvVar(envVars.IdpAudience),
algorithms: [JWT_ALGORITHM], algorithms: [JWT_ALGORITHM],
credentialsRequired: false, credentialsRequired: false,
requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD,
@ -75,7 +75,7 @@ const verifyJwtToken = expressjwt({
*/ */
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
if (!req.jwtPayload) { if (!req.jwtPayload) {
return; return undefined;
} }
const issuer = req.jwtPayload.iss; const issuer = req.jwtPayload.iss;
let accountType: 'student' | 'teacher'; let accountType: 'student' | 'teacher';
@ -85,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
} else if (issuer === idpConfigs.teacher.issuer) { } else if (issuer === idpConfigs.teacher.issuer) {
accountType = 'teacher'; accountType = 'teacher';
} else { } else {
return; return undefined;
} }
return { return {
accountType: accountType, accountType: accountType,
username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!,
@ -101,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
* Add the AuthenticationInfo object with the information about the current authentication to the request in order * Add the AuthenticationInfo object with the information about the current authentication to the request in order
* to avoid that the routers have to deal with the JWT token. * to avoid that the routers have to deal with the JWT token.
*/ */
const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => { function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void {
req.auth = getAuthenticationInfo(req); req.auth = getAuthenticationInfo(req);
next(); next();
}; }
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
@ -114,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
* to true. * to true.
*/ */
export const authorize = export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
(accessCondition: (auth: AuthenticationInfo) => boolean) => return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
(req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
if (!req.auth) { if (!req.auth) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} else if (!accessCondition(req.auth)) { } else if (!accessCondition(req.auth)) {
@ -125,6 +125,7 @@ export const authorize =
next(); next();
} }
}; };
}
/** /**
* Middleware which rejects all unauthenticated users, but accepts all authenticated users. * Middleware which rejects all unauthenticated users, but accepts all authenticated users.

View file

@ -1,11 +1,11 @@
/** /**
* Object with information about the user who is currently logged in. * Object with information about the user who is currently logged in.
*/ */
export type AuthenticationInfo = { export interface AuthenticationInfo {
accountType: 'student' | 'teacher'; accountType: 'student' | 'teacher';
username: string; username: string;
name?: string; name?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
email?: string; email?: string;
}; }

View file

@ -1,7 +1,7 @@
import cors from 'cors'; import cors from 'cors';
import { EnvVars, getEnvVar } from '../util/envvars.js'; import { envVars, getEnvVar } from '../util/envVars.js';
export default cors({ export default cors({
origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','), origin: getEnvVar(envVars.CorsAllowedOrigins).split(','),
allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','), allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','),
}); });

View file

@ -4,7 +4,7 @@ import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-sta
const logger: Logger = getLogger(); const logger: Logger = getLogger();
export function errorHandler(err: unknown, req: Request, res: Response, _: NextFunction): void { export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void {
if (err instanceof ExceptionWithHttpState) { if (err instanceof ExceptionWithHttpState) {
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
res.status(err.status).json(err); res.status(err.status).json(err);

View file

@ -1,6 +1,6 @@
import { LoggerOptions, Options } from '@mikro-orm/core'; import { LoggerOptions, Options } from '@mikro-orm/core';
import { PostgreSqlDriver } from '@mikro-orm/postgresql'; import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; import { envVars, getEnvVar, getNumericEnvVar } from './util/envVars.js';
import { SqliteDriver } from '@mikro-orm/sqlite'; import { SqliteDriver } from '@mikro-orm/sqlite';
import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; import { MikroOrmLogger } from './logging/mikroOrmLogger.js';
@ -42,11 +42,11 @@ const entities = [
Question, Question,
]; ];
function config(testingMode: boolean = false): Options { function config(testingMode = false): Options {
if (testingMode) { if (testingMode) {
return { return {
driver: SqliteDriver, driver: SqliteDriver,
dbName: getEnvVar(EnvVars.DbName), dbName: getEnvVar(envVars.DbName),
subscribers: [new SqliteAutoincrementSubscriber()], subscribers: [new SqliteAutoincrementSubscriber()],
entities: entities, entities: entities,
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`. persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
@ -54,23 +54,23 @@ function config(testingMode: boolean = false): Options {
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION) // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint) // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
dynamicImportProvider: (id) => import(id), dynamicImportProvider: async (id) => import(id),
}; };
} }
return { return {
driver: PostgreSqlDriver, driver: PostgreSqlDriver,
host: getEnvVar(EnvVars.DbHost), host: getEnvVar(envVars.DbHost),
port: getNumericEnvVar(EnvVars.DbPort), port: getNumericEnvVar(envVars.DbPort),
dbName: getEnvVar(EnvVars.DbName), dbName: getEnvVar(envVars.DbName),
user: getEnvVar(EnvVars.DbUsername), user: getEnvVar(envVars.DbUsername),
password: getEnvVar(EnvVars.DbPassword), password: getEnvVar(envVars.DbPassword),
entities: entities, entities: entities,
persistOnCreate: false, // Do not implicitly save entities when they are created via `create`. persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
// EntitiesTs: entitiesTs, // EntitiesTs: entitiesTs,
// Logging // Logging
debug: getEnvVar(EnvVars.LogLevel) === 'debug', debug: getEnvVar(envVars.LogLevel) === 'debug',
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
}; };
} }

View file

@ -1,10 +1,10 @@
import { EntityManager, MikroORM } from '@mikro-orm/core'; import { EntityManager, MikroORM } from '@mikro-orm/core';
import config from './mikro-orm.config.js'; import config from './mikro-orm.config.js';
import { EnvVars, getEnvVar } from './util/envvars.js'; import { envVars, getEnvVar } from './util/envVars.js';
import { getLogger, Logger } from './logging/initalize.js'; import { getLogger, Logger } from './logging/initalize.js';
let orm: MikroORM | undefined; let orm: MikroORM | undefined;
export async function initORM(testingMode: boolean = false) { export async function initORM(testingMode = false): Promise<void> {
const logger: Logger = getLogger(); const logger: Logger = getLogger();
logger.info('Initializing ORM'); logger.info('Initializing ORM');
@ -12,7 +12,7 @@ export async function initORM(testingMode: boolean = false) {
orm = await MikroORM.init(config(testingMode)); orm = await MikroORM.init(config(testingMode));
// Update the database scheme if necessary and enabled. // Update the database scheme if necessary and enabled.
if (getEnvVar(EnvVars.DbUpdate)) { if (getEnvVar(envVars.DbUpdate)) {
await orm.schema.updateSchema(); await orm.schema.updateSchema();
} else { } else {
const diff = await orm.schema.getUpdateSchemaSQL(); const diff = await orm.schema.getUpdateSchemaSQL();

View file

@ -19,7 +19,7 @@ router.get('/:id', getAssignmentHandler);
router.get('/:id/submissions', getAssignmentsSubmissionsHandler); router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
router.get('/:id/questions', (req, res) => { router.get('/:id/questions', (_req, res) => {
res.json({ res.json({
questions: ['0'], questions: ['0'],
}); });

View file

@ -4,21 +4,21 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut
const router = express.Router(); const router = express.Router();
// Returns auth configuration for frontend // Returns auth configuration for frontend
router.get('/config', (req, res) => { router.get('/config', (_req, res) => {
res.json(getFrontendAuthConfig()); res.json(getFrontendAuthConfig());
}); });
router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => { router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
res.json({ message: 'If you see this, you should be authenticated!' }); res.json({ message: 'If you see this, you should be authenticated!' });
}); });
router.get('/testStudentsOnly', studentsOnly, (req, res) => { router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
/* #swagger.security = [{ "student": [ ] }] */ /* #swagger.security = [{ "student": [ ] }] */
res.json({ message: 'If you see this, you should be a student!' }); res.json({ message: 'If you see this, you should be a student!' });
}); });
router.get('/testTeachersOnly', teachersOnly, (req, res) => { router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
/* #swagger.security = [{ "teacher": [ ] }] */ /* #swagger.security = [{ "teacher": [ ] }] */
res.json({ message: 'If you see this, you should be a teacher!' }); res.json({ message: 'If you see this, you should be a teacher!' });
}); });

View file

@ -11,10 +11,10 @@ router.post('/', createGroupHandler);
// Information about a group (members, ... [TODO DOC]) // Information about a group (members, ... [TODO DOC])
router.get('/:groupid', getGroupHandler); router.get('/:groupid', getGroupHandler);
router.get('/:groupid', getGroupSubmissionsHandler); router.get('/:groupid/submissions', getGroupSubmissionsHandler);
// The list of questions a group has made // The list of questions a group has made
router.get('/:id/questions', (req, res) => { router.get('/:id/questions', (_req, res) => {
res.json({ res.json({
questions: ['0'], questions: ['0'],
}); });

View 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;

View file

@ -7,8 +7,11 @@ import {
getStudentClassesHandler, getStudentClassesHandler,
getStudentGroupsHandler, getStudentGroupsHandler,
getStudentHandler, getStudentHandler,
getStudentQuestionsHandler,
getStudentSubmissionsHandler, getStudentSubmissionsHandler,
} from '../controllers/students.js'; } from '../controllers/students.js';
import joinRequestRouter from './student-join-requests.js';
const router = express.Router(); const router = express.Router();
// Root endpoint used to search objects // Root endpoint used to search objects
@ -16,30 +19,26 @@ router.get('/', getAllStudentsHandler);
router.post('/', createStudentHandler); router.post('/', createStudentHandler);
router.delete('/', deleteStudentHandler);
router.delete('/:username', deleteStudentHandler); router.delete('/:username', deleteStudentHandler);
// Information about a student's profile // Information about a student's profile
router.get('/:username', getStudentHandler); router.get('/:username', getStudentHandler);
// The list of classes a student is in // 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 // 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 // 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 // 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 // A list of questions a user has created
router.get('/:id/questions', (req, res) => { router.get('/:username/questions', getStudentQuestionsHandler);
res.json({
questions: ['0'], router.use('/:username/joinRequests', joinRequestRouter);
});
});
export default router; export default router;

View file

@ -3,7 +3,7 @@ import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler
const router = express.Router({ mergeParams: true }); const router = express.Router({ mergeParams: true });
// Root endpoint used to search objects // Root endpoint used to search objects
router.get('/', (req, res) => { router.get('/', (_req, res) => {
res.json({ res.json({
submissions: ['0', '1'], submissions: ['0', '1'],
}); });

View file

@ -3,10 +3,12 @@ import {
createTeacherHandler, createTeacherHandler,
deleteTeacherHandler, deleteTeacherHandler,
getAllTeachersHandler, getAllTeachersHandler,
getStudentJoinRequestHandler,
getTeacherClassHandler, getTeacherClassHandler,
getTeacherHandler, getTeacherHandler,
getTeacherQuestionHandler, getTeacherQuestionHandler,
getTeacherStudentHandler, getTeacherStudentHandler,
updateStudentJoinRequestHandler,
} from '../controllers/teachers.js'; } from '../controllers/teachers.js';
const router = express.Router(); const router = express.Router();
@ -15,8 +17,6 @@ router.get('/', getAllTeachersHandler);
router.post('/', createTeacherHandler); router.post('/', createTeacherHandler);
router.delete('/', deleteTeacherHandler);
router.get('/:username', getTeacherHandler); router.get('/:username', getTeacherHandler);
router.delete('/:username', deleteTeacherHandler); router.delete('/:username', deleteTeacherHandler);
@ -27,8 +27,12 @@ router.get('/:username/students', getTeacherStudentHandler);
router.get('/:username/questions', getTeacherQuestionHandler); 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 // Invitations to other classes a teacher received
router.get('/:id/invitations', (req, res) => { router.get('/:id/invitations', (_req, res) => {
res.json({ res.json({
invitations: ['0'], invitations: ['0'],
}); });

View file

@ -1,6 +1,9 @@
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.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[]> { export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
@ -37,7 +40,7 @@ export async function createAssignment(classid: string, assignmentData: Assignme
return mapToAssignmentDTO(newAssignment); return mapToAssignmentDTO(newAssignment);
} catch (e) { } catch (e) {
console.error(e); getLogger().error(e);
return null; return null;
} }
} }
@ -83,7 +86,7 @@ export async function getAssignmentsSubmissions(
const groups = await groupRepository.findAllGroupsForAssignment(assignment); const groups = await groupRepository.findAllGroupsForAssignment(assignment);
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
if (full) { if (full) {
return submissions.map(mapToSubmissionDTO); return submissions.map(mapToSubmissionDTO);

View file

@ -1,11 +1,27 @@
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; import { mapToClassDTO } from '../interfaces/class.js';
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; import { mapToStudentDTO } from '../interfaces/student.js';
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js';
import { getLogger } from '../logging/initalize.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(); 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[]> { export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[]> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); 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> { export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> {
const teacherRepository = getTeacherRepository(); const teacherRepository = getTeacherRepository();
const teacherUsernames = classData.teachers || []; const teacherUsernames = classData.teachers || [];
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null); const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter(
(teacher) => teacher !== null
);
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const studentUsernames = classData.students || []; const studentUsernames = classData.students || [];
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student !== null
);
const classRepository = getClassRepository(); const classRepository = getClassRepository();

View file

@ -6,8 +6,11 @@ import {
getSubmissionRepository, getSubmissionRepository,
} from '../data/repositories.js'; } from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.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> { export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
@ -42,9 +45,11 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student !== null
);
console.log(members); getLogger().debug(members);
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const cls = await classRepository.findById(classid); const cls = await classRepository.findById(classid);
@ -70,7 +75,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
return newGroup; return newGroup;
} catch (e) { } catch (e) {
console.log(e); getLogger().error(e);
return null; return null;
} }
} }
@ -94,8 +99,7 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu
const groups = await groupRepository.findAllGroupsForAssignment(assignment); const groups = await groupRepository.findAllGroupsForAssignment(assignment);
if (full) { if (full) {
console.log('full'); getLogger().debug({ full: full, groups: groups });
console.log(groups);
return groups.map(mapToGroupDTO); return groups.map(mapToGroupDTO);
} }

View file

@ -1,6 +1,13 @@
import { DWENGO_API_BASE } from '../config.js'; import { DWENGO_API_BASE } from '../config.js';
import { fetchWithLogging } from '../util/api-helper.js'; import { fetchWithLogging } from '../util/api-helper.js';
import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js';
import {
FilteredLearningObject,
LearningObjectMetadata,
LearningObjectNode,
LearningPathResponse,
} from '@dwengo-1/common/interfaces/learning-content';
import { getLogger } from '../logging/initalize.js';
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
return { return {
@ -37,7 +44,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
); );
if (!metadata) { if (!metadata) {
console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`);
return null; return null;
} }
@ -48,7 +55,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
/** /**
* Generic function to fetch learning paths * Generic function to fetch learning paths
*/ */
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> { function fetchLearningPaths(_arg0: string[], _language: string, _arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
} }
@ -60,7 +67,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri
const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`); const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`);
if (!learningPathResponse.success || !learningPathResponse.data?.length) { if (!learningPathResponse.success || !learningPathResponse.data?.length) {
console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); getLogger().error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
return []; return [];
} }
@ -74,7 +81,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri
objects.filter((obj): obj is FilteredLearningObject => obj !== null) objects.filter((obj): obj is FilteredLearningObject => obj !== null)
); );
} catch (error) { } catch (error) {
console.error('❌ Error fetching learning objects:', error); getLogger().error('❌ Error fetching learning objects:', error);
return []; return [];
} }
} }

View file

@ -1,9 +1,10 @@
import { getAttachmentRepository } from '../../data/repositories.js'; import { getAttachmentRepository } from '../../data/repositories.js';
import { Attachment } from '../../entities/content/attachment.entity.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 = { const attachmentService = {
getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> { async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
const attachmentRepo = getAttachmentRepository(); const attachmentRepo = getAttachmentRepository();
if (learningObjectId.version) { if (learningObjectId.version) {

View file

@ -1,13 +1,12 @@
import { LearningObjectProvider } from './learning-object-provider.js'; import { LearningObjectProvider } from './learning-object-provider.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js'; import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js';
import { Language } from '../../entities/content/language.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { getUrlStringForLearningObject } from '../../util/links.js'; import { getUrlStringForLearningObject } from '../../util/links.js';
import processingService from './processing/processing-service.js'; import processingService from './processing/processing-service.js';
import { NotFoundError } from '@mikro-orm/core'; import { NotFoundError } from '@mikro-orm/core';
import learningObjectService from './learning-object-service.js'; import learningObjectService from './learning-object-service.js';
import { getLogger, Logger } from '../../logging/initalize.js'; import { getLogger, Logger } from '../../logging/initalize.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
const logger: Logger = getLogger(); 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(); const learningObjectRepo = getLearningObjectRepository();
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
} }
/** /**
@ -65,11 +64,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
const learningObjectRepo = getLearningObjectRepository(); const learningObjectRepo = getLearningObjectRepository();
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
if (!learningObject) { if (!learningObject) {
return null; return null;
} }
return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id)); return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id));
}, },
/** /**
@ -96,7 +95,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
throw new NotFoundError('The learning path with the given ID could not be found.'); throw new NotFoundError('The learning path with the given ID could not be found.');
} }
const learningObjects = await Promise.all( const learningObjects = await Promise.all(
learningPath.nodes.map((it) => { learningPath.nodes.map(async (it) => {
const learningObject = learningObjectService.getLearningObjectById({ const learningObject = learningObjectService.getLearningObjectById({
hruid: it.learningObjectHruid, hruid: it.learningObjectHruid,
language: it.language, language: it.language,

View file

@ -1,5 +1,8 @@
import { DWENGO_API_BASE } from '../../config.js'; import { DWENGO_API_BASE } from '../../config.js';
import { fetchWithLogging } from '../../util/api-helper.js'; import { fetchWithLogging } from '../../util/api-helper.js';
import 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 { import {
FilteredLearningObject, FilteredLearningObject,
LearningObjectIdentifier, LearningObjectIdentifier,
@ -7,10 +10,7 @@ import {
LearningObjectNode, LearningObjectNode,
LearningPathIdentifier, LearningPathIdentifier,
LearningPathResponse, LearningPathResponse,
} from '../../interfaces/learning-content.js'; } from '@dwengo-1/common/interfaces/learning-content';
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';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
@ -66,12 +66,13 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full
} }
const objects = await Promise.all( const objects = await Promise.all(
nodes.map(async (node) => nodes.map(async (node) => {
dwengoApiLearningObjectProvider.getLearningObjectById({ const learningObjectId: LearningObjectIdentifier = {
hruid: node.learningobject_hruid, hruid: node.learningobject_hruid,
language: learningPathId.language, language: learningPathId.language,
};
return dwengoApiLearningObjectProvider.getLearningObjectById(learningObjectId);
}) })
)
); );
return objects.filter((obj): obj is FilteredLearningObject => obj !== null); return objects.filter((obj): obj is FilteredLearningObject => obj !== null);
} catch (error) { } catch (error) {
@ -90,7 +91,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
metadataUrl, metadataUrl,
`Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, `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> { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`;
const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, {
params: id, params: { ...id },
}); });
if (!html) { if (!html) {

View file

@ -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 { export interface LearningObjectProvider {
/** /**

View file

@ -1,11 +1,11 @@
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js'; import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js';
import { LearningObjectProvider } from './learning-object-provider.js'; import { LearningObjectProvider } from './learning-object-provider.js';
import { EnvVars, getEnvVar } from '../../util/envvars.js'; import { envVars, getEnvVar } from '../../util/envVars.js';
import databaseLearningObjectProvider from './database-learning-object-provider.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { function getProvider(id: LearningObjectIdentifier): LearningObjectProvider {
if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) {
return databaseLearningObjectProvider; return databaseLearningObjectProvider;
} }
return dwengoApiLearningObjectProvider; return dwengoApiLearningObjectProvider;
@ -18,28 +18,28 @@ const learningObjectService = {
/** /**
* Fetches a single learning object by its HRUID * Fetches a single learning object by its HRUID
*/ */
getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> { async getLearningObjectById(id: LearningObjectIdentifier): Promise<FilteredLearningObject | null> {
return getProvider(id).getLearningObjectById(id); return getProvider(id).getLearningObjectById(id);
}, },
/** /**
* Fetch full learning object data (metadata) * Fetch full learning object data (metadata)
*/ */
getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> { async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise<FilteredLearningObject[]> {
return getProvider(id).getLearningObjectsFromPath(id); return getProvider(id).getLearningObjectsFromPath(id);
}, },
/** /**
* Fetch only learning object HRUIDs * Fetch only learning object HRUIDs
*/ */
getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> { async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise<string[]> {
return getProvider(id).getLearningObjectIdsFromPath(id); return getProvider(id).getLearningObjectIdsFromPath(id);
}, },
/** /**
* Obtain a HTML-rendering of the learning object with the given identifier (as a string). * Obtain a HTML-rendering of the learning object with the given identifier (as a string).
*/ */
getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
return getProvider(id).getLearningObjectHTML(id); return getProvider(id).getLearningObjectHTML(id);
}, },
}; };

View file

@ -14,7 +14,7 @@ class AudioProcessor extends StringProcessor {
super(DwengoContentType.AUDIO_MPEG); super(DwengoContentType.AUDIO_MPEG);
} }
protected renderFn(audioUrl: string): string { override renderFn(audioUrl: string): string {
return DOMPurify.sanitize(`<audio controls> return DOMPurify.sanitize(`<audio controls>
<source src="${audioUrl}" type=${type}> <source src="${audioUrl}" type=${type}>
Your browser does not support the audio element. Your browser does not support the audio element.

View file

@ -15,7 +15,7 @@ class ExternProcessor extends StringProcessor {
super(DwengoContentType.EXTERN); super(DwengoContentType.EXTERN);
} }
override renderFn(externURL: string) { override renderFn(externURL: string): string {
if (!isValidHttpUrl(externURL)) { if (!isValidHttpUrl(externURL)) {
throw new ProcessingError('The url is not valid: ' + externURL); throw new ProcessingError('The url is not valid: ' + externURL);
} }

View file

@ -32,7 +32,7 @@ class GiftProcessor extends StringProcessor {
super(DwengoContentType.GIFT); super(DwengoContentType.GIFT);
} }
override renderFn(giftString: string) { override renderFn(giftString: string): string {
const quizQuestions: GIFTQuestion[] = parse(giftString); const quizQuestions: GIFTQuestion[] = parse(giftString);
let html = "<div class='learning-object-gift'>\n"; let html = "<div class='learning-object-gift'>\n";

View file

@ -3,7 +3,7 @@ import { Category } from 'gift-pegjs';
import { ProcessingError } from '../../processing-error.js'; import { ProcessingError } from '../../processing-error.js';
export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> { export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> {
render(question: Category, questionNumber: number): string { override render(_question: Category, _questionNumber: number): string {
throw new ProcessingError("The question type 'Category' is not supported yet!"); throw new ProcessingError("The question type 'Category' is not supported yet!");
} }
} }

View file

@ -3,7 +3,7 @@ import { Description } from 'gift-pegjs';
import { ProcessingError } from '../../processing-error.js'; import { ProcessingError } from '../../processing-error.js';
export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> { export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> {
render(question: Description, questionNumber: number): string { override render(_question: Description, _questionNumber: number): string {
throw new ProcessingError("The question type 'Description' is not supported yet!"); throw new ProcessingError("The question type 'Description' is not supported yet!");
} }
} }

View file

@ -2,7 +2,7 @@ import { GIFTQuestionRenderer } from './gift-question-renderer.js';
import { Essay } from 'gift-pegjs'; import { Essay } from 'gift-pegjs';
export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> { export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> {
render(question: Essay, questionNumber: number): string { override render(question: Essay, questionNumber: number): string {
let renderedHtml = ''; let renderedHtml = '';
if (question.title) { if (question.title) {
renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`; renderedHtml += `<h2 class='gift-title' id='gift-q${questionNumber}-title'>${question.title}</h2>\n`;

Some files were not shown because too many files have changed in this diff Show more