Merge remote-tracking branch 'origin/dev' into feat/endpoints-finaliseren-tests-backend-adriaan
# Conflicts: # backend/src/controllers/assignments.ts # backend/src/controllers/classes.ts # backend/src/controllers/groups.ts # backend/src/controllers/questions.ts # backend/src/controllers/submissions.ts # backend/src/data/assignments/submission-repository.ts # backend/src/data/users/student-repository.ts # backend/src/services/classes.ts # backend/src/services/submissions.ts # backend/tests/controllers/classes.test.ts
This commit is contained in:
		
						commit
						ded1a5908e
					
				
					 375 changed files with 23425 additions and 7033 deletions
				
			
		
							
								
								
									
										63
									
								
								.github/workflows/backend-testing.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								.github/workflows/backend-testing.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | # 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", "main" ] | ||||||
|  |     paths: | ||||||
|  |       - 'backend/src/**.[jt]s' | ||||||
|  |       - 'backend/tests/**.[jt]s' | ||||||
|  |       - 'backend/vitest.config.ts' | ||||||
|  |   pull_request: | ||||||
|  |     branches: [ "dev", "main" ] | ||||||
|  |     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] | ||||||
|  | 
 | ||||||
|  |     permissions: | ||||||
|  |       # Required to checkout the code | ||||||
|  |       contents: read | ||||||
|  |       # Required to put a comment into the pull-request | ||||||
|  |       pull-requests: write | ||||||
|  | 
 | ||||||
|  |     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 build | ||||||
|  |     - run: npm run test:coverage -w backend | ||||||
|  |     - name: 'Report Backend Coverage' | ||||||
|  |       # Set if: always() to also generate the report if tests are failing | ||||||
|  |       # Only works if you set `reportOnFailure: true` in your vite config as specified above | ||||||
|  |       if: always()  | ||||||
|  |       uses:  davelosert/vitest-coverage-report-action@v2 | ||||||
|  |       with: | ||||||
|  |         name: 'Backend' | ||||||
|  |         json-summary-path: './backend/coverage/coverage-summary.json' | ||||||
|  |         json-final-path: './backend/coverage/coverage-final.json' | ||||||
|  |         vite-config-path: './backend/vitest.config.ts' | ||||||
|  |         file-coverage-mode: all | ||||||
							
								
								
									
										5
									
								
								.github/workflows/deployment.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/deployment.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -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 && cp /home/dev/.idp.env config/idp/.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.yml -f compose.production.yml up --build -d | ||||||
|          |          | ||||||
							
								
								
									
										72
									
								
								.github/workflows/frontend-testing.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								.github/workflows/frontend-testing.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | ||||||
|  | # 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", "main" ] | ||||||
|  |       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", "main" ] | ||||||
|  |       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] | ||||||
|  | 
 | ||||||
|  |     permissions: | ||||||
|  |       # Required to checkout the code | ||||||
|  |       contents: read | ||||||
|  |       # Required to put a comment into the pull-request | ||||||
|  |       pull-requests: write | ||||||
|  | 
 | ||||||
|  |     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 build | ||||||
|  |     - run: npm run test:coverage -w frontend | ||||||
|  |     - name: 'Report Frontend Coverage' | ||||||
|  |       # Set if: always() to also generate the report if tests are failing | ||||||
|  |       # Only works if you set `reportOnFailure: true` in your vite config as specified above | ||||||
|  |       if: always()  | ||||||
|  |       uses:  davelosert/vitest-coverage-report-action@v2 | ||||||
|  |       with: | ||||||
|  |         name: 'Frontend' | ||||||
|  |         json-summary-path: './frontend/coverage/coverage-summary.json' | ||||||
|  |         json-final-path: './frontend/coverage/coverage-final.json' | ||||||
|  |         vite-config-path: './frontend/vitest.config.ts' | ||||||
|  |         file-coverage-mode: all | ||||||
							
								
								
									
										8
									
								
								.github/workflows/lint-action.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/lint-action.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -4,13 +4,11 @@ on: | ||||||
|     # Trigger the workflow on push or pull request, |     # Trigger the workflow on push or pull request, | ||||||
|     # but only for the main branch |     # but only for the main branch | ||||||
|     push: |     push: | ||||||
|         branches: |         branches: [ "dev", "main" ] | ||||||
|             - dev |  | ||||||
|     # Replace pull_request with pull_request_target if you |     # Replace pull_request with pull_request_target if you | ||||||
|     # plan to use this action with forks, see the Limitations section |     # plan to use this action with forks, see the Limitations section | ||||||
|     pull_request: |     pull_request: | ||||||
|         branches: |         branches: [ "dev", "main" ] | ||||||
|             - dev |  | ||||||
|         types: ["synchronize", "ready_for_review", "opened", "reopened"] |         types: ["synchronize", "ready_for_review", "opened", "reopened"] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -43,6 +41,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}' | ||||||
							
								
								
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -737,4 +737,6 @@ flycheck_*.el | ||||||
| # network security | # network security | ||||||
| /network-security.data | /network-security.data | ||||||
| 
 | 
 | ||||||
| 
 | docs/.venv | ||||||
|  | idp_data/h2/keycloakdb.mv.db | ||||||
|  | idp_data/h2/keycloakdb.trace.db | ||||||
|  |  | ||||||
|  | @ -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
									
								
							
							
						
						
									
										21
									
								
								backend/.env.staging
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | PORT=3000 | ||||||
|  | DWENGO_DB_HOST=db | ||||||
|  | DWENGO_DB_PORT=5432 | ||||||
|  | DWENGO_DB_USERNAME=postgres | ||||||
|  | DWENGO_DB_PASSWORD=postgres | ||||||
|  | DWENGO_DB_UPDATE=false | ||||||
|  | 
 | ||||||
|  | DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student | ||||||
|  | DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo | ||||||
|  | DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs | ||||||
|  | DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher | ||||||
|  | DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | ||||||
|  | DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs | ||||||
|  | 
 | ||||||
|  | # Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production! | ||||||
|  | #DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost | ||||||
|  | DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080 | ||||||
|  | 
 | ||||||
|  | # Logging and monitoring | ||||||
|  | 
 | ||||||
|  | LOKI_HOST=http://logging:3102 | ||||||
|  | @ -1,3 +1,22 @@ | ||||||
| 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 | ||||||
|  | 
 | ||||||
|  | DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student | ||||||
|  | DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo | ||||||
|  | DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs | ||||||
|  | DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher | ||||||
|  | DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | ||||||
|  | DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs | ||||||
|  | 
 | ||||||
|  | DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:9876,* | ||||||
|  |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| # |  | ||||||
| # Test environment configuration |  | ||||||
| # |  | ||||||
| # Should not need to be modified. |  | ||||||
| # See .env.example for more information. |  | ||||||
| # |  | ||||||
| 
 |  | ||||||
| ### Dwengo ### |  | ||||||
| 
 |  | ||||||
| DWENGO_PORT=3000 |  | ||||||
| 
 |  | ||||||
| DWENGO_DB_NAME=":memory:" |  | ||||||
| DWENGO_DB_UPDATE=true |  | ||||||
|  | @ -1,38 +1,51 @@ | ||||||
| FROM node:22 AS build-stage | 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 and docs | ||||||
|  | COPY common/package.json ./common/ | ||||||
|  | COPY docs/package.json ./docs/ | ||||||
| 
 | 
 | ||||||
| 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 --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json | ||||||
|  | 
 | ||||||
|  | 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 ./backend/i18n ./backend/i18n | ||||||
| COPY ./backend/i18n /app/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"] | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -1,7 +0,0 @@ | ||||||
| // Can be placed in dotenv but found it redundant
 |  | ||||||
| // Import dotenv from "dotenv";
 |  | ||||||
| // Load .env file
 |  | ||||||
| // Dotenv.config();
 |  | ||||||
| export const DWENGO_API_BASE = 'https://dwengo.org/backend/api'; |  | ||||||
| export const FALLBACK_LANG = 'nl'; |  | ||||||
| export const FALLBACK_SEQ_NUM = 1; |  | ||||||
|  | @ -8,14 +8,4 @@ export default [ | ||||||
|             globals: globals.node, |             globals: globals.node, | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
| 
 |  | ||||||
|     { |  | ||||||
|         files: ['tests/**/*.ts'], |  | ||||||
|         languageOptions: { |  | ||||||
|             globals: globals.node, |  | ||||||
|         }, |  | ||||||
|         rules: { |  | ||||||
|             'no-console': 'off', |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | @ -1,24 +1,28 @@ | ||||||
| { | { | ||||||
|     "name": "dwengo-1-backend", |     "name": "@dwengo-1/backend", | ||||||
|     "version": "0.1.1", |     "version": "0.2.0", | ||||||
|     "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", |         "predev": "tsc --build ../common/tsconfig.json", | ||||||
|  |         "dev": "cross-env NODE_ENV=development tsx tool/seed.ts && 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": "tsx ../docs/api/generate.ts && npm run build", | ||||||
|  |         "test:unit": "vitest --run", | ||||||
|  |         "test:coverage": "vitest --run --coverage.enabled true" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@mikro-orm/core": "6.4.9", |         "@mikro-orm/core": "6.4.12", | ||||||
|         "@mikro-orm/knex": "6.4.9", |         "@mikro-orm/knex": "6.4.12", | ||||||
|         "@mikro-orm/postgresql": "6.4.9", |         "@mikro-orm/postgresql": "6.4.12", | ||||||
|         "@mikro-orm/reflection": "6.4.9", |         "@mikro-orm/reflection": "6.4.12", | ||||||
|         "@mikro-orm/sqlite": "6.4.9", |         "@mikro-orm/sqlite": "6.4.12", | ||||||
|         "axios": "^1.8.2", |         "axios": "^1.8.2", | ||||||
|         "cors": "^2.8.5", |         "cors": "^2.8.5", | ||||||
|         "cross": "^1.0.0", |         "cross": "^1.0.0", | ||||||
|  | @ -33,6 +37,7 @@ | ||||||
|         "jwks-rsa": "^3.1.0", |         "jwks-rsa": "^3.1.0", | ||||||
|         "loki-logger-ts": "^1.0.2", |         "loki-logger-ts": "^1.0.2", | ||||||
|         "marked": "^15.0.7", |         "marked": "^15.0.7", | ||||||
|  |         "nanoid": "^5.1.5", | ||||||
|         "response-time": "^2.3.3", |         "response-time": "^2.3.3", | ||||||
|         "swagger-ui-express": "^5.0.1", |         "swagger-ui-express": "^5.0.1", | ||||||
|         "uuid": "^11.1.0", |         "uuid": "^11.1.0", | ||||||
|  | @ -40,7 +45,7 @@ | ||||||
|         "winston-loki": "^6.1.3" |         "winston-loki": "^6.1.3" | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@mikro-orm/cli": "6.4.9", |         "@mikro-orm/cli": "6.4.12", | ||||||
|         "@types/cors": "^2.8.17", |         "@types/cors": "^2.8.17", | ||||||
|         "@types/express": "^5.0.0", |         "@types/express": "^5.0.0", | ||||||
|         "@types/js-yaml": "^4.0.9", |         "@types/js-yaml": "^4.0.9", | ||||||
|  |  | ||||||
|  | @ -5,15 +5,16 @@ import cors from './middleware/cors.js'; | ||||||
| import { getLogger, Logger } from './logging/initalize.js'; | import { getLogger, Logger } from './logging/initalize.js'; | ||||||
| import { responseTimeLogger } from './logging/responseTimeLogger.js'; | import { responseTimeLogger } from './logging/responseTimeLogger.js'; | ||||||
| import responseTime from 'response-time'; | import responseTime from 'response-time'; | ||||||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | import { envVars, getNumericEnvVar } from './util/envVars.js'; | ||||||
| import apiRouter from './routes/router.js'; | import apiRouter from './routes/router.js'; | ||||||
| import swaggerMiddleware from './swagger.js'; | import swaggerMiddleware from './swagger.js'; | ||||||
| import swaggerUi from 'swagger-ui-express'; | import swaggerUi from 'swagger-ui-express'; | ||||||
|  | import { errorHandler } from './middleware/error-handling/error-handler.js'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
| const app: Express = express(); | const app: Express = express(); | ||||||
| const port: string | number = getNumericEnvVar(EnvVars.Port); | const port: string | number = getNumericEnvVar(envVars.Port); | ||||||
| 
 | 
 | ||||||
| app.use(express.json()); | app.use(express.json()); | ||||||
| app.use(cors); | app.use(cors); | ||||||
|  | @ -26,7 +27,9 @@ app.use('/api', apiRouter); | ||||||
| // Swagger
 | // Swagger
 | ||||||
| app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); | app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); | ||||||
| 
 | 
 | ||||||
| async function startServer() { | app.use(errorHandler); | ||||||
|  | 
 | ||||||
|  | async function startServer(): Promise<void> { | ||||||
|     await initORM(); |     await initORM(); | ||||||
| 
 | 
 | ||||||
|     app.listen(port, () => { |     app.listen(port, () => { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| 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; | ||||||
|  | export const FALLBACK_VERSION_NUM = 1; | ||||||
|  |  | ||||||
							
								
								
									
										99
									
								
								backend/src/controllers/answers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								backend/src/controllers/answers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
|  | import { getLearningObjectId, getQuestionId } from './questions.js'; | ||||||
|  | import { createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer } from '../services/answers.js'; | ||||||
|  | import { FALLBACK_SEQ_NUM } from '../config.js'; | ||||||
|  | import { AnswerData } from '@dwengo-1/common/interfaces/answer'; | ||||||
|  | 
 | ||||||
|  | export async function getAllAnswersHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const answers = await getAnswersByQuestion(questionId, full); | ||||||
|  | 
 | ||||||
|  |     res.json({ answers }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getAnswerHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const seqAnswer = req.params.seqAnswer; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; | ||||||
|  |     const answer = await getAnswer(questionId, sequenceNumber); | ||||||
|  | 
 | ||||||
|  |     res.json({ answer }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createAnswerHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const author = req.body.author as string; | ||||||
|  |     const content = req.body.content as string; | ||||||
|  |     requireFields({ author, content }); | ||||||
|  | 
 | ||||||
|  |     const answerData = req.body as AnswerData; | ||||||
|  | 
 | ||||||
|  |     const answer = await createAnswer(questionId, answerData); | ||||||
|  | 
 | ||||||
|  |     res.json({ answer }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteAnswerHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const seqAnswer = req.params.seqAnswer; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; | ||||||
|  |     const answer = await deleteAnswer(questionId, sequenceNumber); | ||||||
|  | 
 | ||||||
|  |     res.json({ answer }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function updateAnswerHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const seqAnswer = req.params.seqAnswer; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const content = req.body.content as string; | ||||||
|  |     requireFields({ content }); | ||||||
|  | 
 | ||||||
|  |     const answerData = req.body as AnswerData; | ||||||
|  | 
 | ||||||
|  |     const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; | ||||||
|  |     const answer = await updateAnswer(questionId, sequenceNumber, answerData); | ||||||
|  | 
 | ||||||
|  |     res.json({ answer }); | ||||||
|  | } | ||||||
|  | @ -1,72 +1,93 @@ | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; | import { | ||||||
| import { AssignmentDTO } from '../interfaces/assignment.js'; |     createAssignment, | ||||||
|  |     deleteAssignment, | ||||||
|  |     getAllAssignments, | ||||||
|  |     getAssignment, | ||||||
|  |     getAssignmentsQuestions, | ||||||
|  |     getAssignmentsSubmissions, | ||||||
|  |     putAssignment, | ||||||
|  | } from '../services/assignments.js'; | ||||||
|  | import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
|  | import { Assignment } from '../entities/assignments/assignment.entity.js'; | ||||||
|  | import { EntityDTO } from '@mikro-orm/core'; | ||||||
| 
 | 
 | ||||||
| // Typescript is annoy with with parameter forwarding from class.ts
 | function getAssignmentParams(req: Request): { classid: string; assignmentNumber: number; full: boolean } { | ||||||
| export async function getAllAssignmentsHandler(req: Request, res: Response): Promise<void> { |  | ||||||
|     const classid = req.params.classid; |     const classid = req.params.classid; | ||||||
|  |     const assignmentNumber = Number(req.params.id); | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ assignmentNumber, classid }); | ||||||
|  | 
 | ||||||
|  |     if (isNaN(assignmentNumber)) { | ||||||
|  |         throw new BadRequestException('Assignment id should be a number'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { classid, assignmentNumber, full }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getAllAssignmentsHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const classId = req.params.classid; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 | 
 | ||||||
|     const assignments = await getAllAssignments(classid, full); |     const assignments = await getAllAssignments(classId, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ assignments }); | ||||||
|         assignments: assignments, |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createAssignmentHandler(req: Request, res: Response): Promise<void> { | export async function createAssignmentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classid = req.params.classid; |     const classid = req.params.classid; | ||||||
|  |     const description = req.body.description; | ||||||
|  |     const language = req.body.language; | ||||||
|  |     const learningPath = req.body.learningPath; | ||||||
|  |     const title = req.body.title; | ||||||
|  | 
 | ||||||
|  |     requireFields({ description, language, learningPath, title }); | ||||||
|  | 
 | ||||||
|     const assignmentData = req.body as AssignmentDTO; |     const assignmentData = req.body as AssignmentDTO; | ||||||
| 
 |  | ||||||
|     if (!assignmentData.description || !assignmentData.language || !assignmentData.learningPath || !assignmentData.title) { |  | ||||||
|         res.status(400).json({ |  | ||||||
|             error: 'Missing one or more required fields: title, description, learningPath, language', |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const assignment = await createAssignment(classid, assignmentData); |     const assignment = await createAssignment(classid, assignmentData); | ||||||
| 
 | 
 | ||||||
|     if (!assignment) { |     res.json({ assignment }); | ||||||
|         res.status(500).json({ error: 'Could not create assignment ' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.status(201).json(assignment); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAssignmentHandler(req: Request, res: Response): Promise<void> { | export async function getAssignmentHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const id = +req.params.id; |     const { classid, assignmentNumber } = getAssignmentParams(req); | ||||||
|     const classid = req.params.classid; |  | ||||||
| 
 | 
 | ||||||
|     if (isNaN(id)) { |     const assignment = await getAssignment(classid, assignmentNumber); | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const assignment = await getAssignment(classid, id); |     res.json({ assignment }); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     if (!assignment) { | export async function putAssignmentHandler(req: Request, res: Response): Promise<void> { | ||||||
|         res.status(404).json({ error: 'Assignment not found' }); |     const { classid, assignmentNumber } = getAssignmentParams(req); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     res.json(assignment); |     const assignmentData = req.body as Partial<EntityDTO<Assignment>>; | ||||||
|  |     const assignment = await putAssignment(classid, assignmentNumber, assignmentData); | ||||||
|  | 
 | ||||||
|  |     res.json({ assignment }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteAssignmentHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const { classid, assignmentNumber } = getAssignmentParams(req); | ||||||
|  | 
 | ||||||
|  |     const assignment = await deleteAssignment(classid, assignmentNumber); | ||||||
|  | 
 | ||||||
|  |     res.json({ assignment }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> { | export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classid = req.params.classid; |     const { classid, assignmentNumber, full } = getAssignmentParams(req); | ||||||
|     const assignmentNumber = +req.params.id; |  | ||||||
|     const full = req.query.full === 'true'; |  | ||||||
| 
 |  | ||||||
|     if (isNaN(assignmentNumber)) { |  | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full); |     const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ submissions }); | ||||||
|         submissions: submissions, | } | ||||||
|     }); | 
 | ||||||
|  | export async function getAssignmentQuestionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const { classid, assignmentNumber, full } = getAssignmentParams(req); | ||||||
|  | 
 | ||||||
|  |     const questions = await getAssignmentsQuestions(classid, assignmentNumber, full); | ||||||
|  | 
 | ||||||
|  |     res.json({ questions }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,33 +1,62 @@ | ||||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | import { UnauthorizedException } from '../exceptions/unauthorized-exception.js'; | ||||||
|  | import { getLogger } from '../logging/initalize.js'; | ||||||
|  | import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js'; | ||||||
|  | import { createOrUpdateStudent } from '../services/students.js'; | ||||||
|  | import { createOrUpdateTeacher } from '../services/teachers.js'; | ||||||
|  | import { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
|  | import { Response } from 'express'; | ||||||
| 
 | 
 | ||||||
| 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'; | ||||||
| 
 | 
 | ||||||
|  | const logger = getLogger(); | ||||||
|  | 
 | ||||||
| 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, | ||||||
|         }, |         }, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> { | ||||||
|  |     const auth = req.auth; | ||||||
|  |     if (!auth) { | ||||||
|  |         throw new UnauthorizedException('Cannot say hello when not authenticated.'); | ||||||
|  |     } | ||||||
|  |     const userData = { | ||||||
|  |         id: auth.username, | ||||||
|  |         username: auth.username, | ||||||
|  |         firstName: auth.firstName ?? '', | ||||||
|  |         lastName: auth.lastName ?? '', | ||||||
|  |     }; | ||||||
|  |     if (auth.accountType === 'student') { | ||||||
|  |         await createOrUpdateStudent(userData); | ||||||
|  |         logger.debug(`Synchronized student ${userData.username} with IDP`); | ||||||
|  |     } else { | ||||||
|  |         await createOrUpdateTeacher(userData); | ||||||
|  |         logger.debug(`Synchronized teacher ${userData.username} with IDP`); | ||||||
|  |     } | ||||||
|  |     res.status(200).send({ message: 'Welcome!' }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,76 +1,132 @@ | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { createClass, getAllClasses, getClass, getClassStudents, getClassTeacherInvitations } from '../services/class.js'; | import { | ||||||
| import { ClassDTO } from '../interfaces/class.js'; |     addClassStudent, | ||||||
|  |     addClassTeacher, | ||||||
|  |     createClass, | ||||||
|  |     deleteClass, | ||||||
|  |     deleteClassStudent, | ||||||
|  |     deleteClassTeacher, | ||||||
|  |     getAllClasses, | ||||||
|  |     getClass, | ||||||
|  |     getClassStudents, | ||||||
|  |     getClassTeacherInvitations, | ||||||
|  |     getClassTeachers, | ||||||
|  |     putClass, | ||||||
|  | } from '../services/classes.js'; | ||||||
|  | import { ClassDTO } from '@dwengo-1/common/interfaces/class'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
|  | import { EntityDTO } from '@mikro-orm/core'; | ||||||
|  | import { Class } from '../entities/classes/class.entity.js'; | ||||||
| 
 | 
 | ||||||
| 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'; | ||||||
|     const classes = await getAllClasses(full); |     const classes = await getAllClasses(full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ classes }); | ||||||
|         classes: classes, |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createClassHandler(req: Request, res: Response): Promise<void> { | export async function createClassHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const displayName = req.body.displayName; | ||||||
|  |     requireFields({ displayName }); | ||||||
|  | 
 | ||||||
|     const classData = req.body as ClassDTO; |     const classData = req.body as ClassDTO; | ||||||
| 
 |  | ||||||
|     if (!classData.displayName) { |  | ||||||
|         res.status(400).json({ |  | ||||||
|             error: 'Missing one or more required fields: displayName', |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const cls = await createClass(classData); |     const cls = await createClass(classData); | ||||||
| 
 | 
 | ||||||
|     if (!cls) { |     res.json({ class: cls }); | ||||||
|         res.status(500).json({ error: 'Something went wrong while creating class' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     res.status(201).json(cls); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getClassHandler(req: Request, res: Response): Promise<void> { | export async function getClassHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classId = req.params.id; |     const classId = req.params.id; | ||||||
|  |     requireFields({ classId }); | ||||||
|  | 
 | ||||||
|     const cls = await getClass(classId); |     const cls = await getClass(classId); | ||||||
| 
 | 
 | ||||||
|     if (!cls) { |     res.json({ class: cls }); | ||||||
|         res.status(404).json({ error: 'Class not found' }); | } | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     res.json(cls); | export async function putClassHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const classId = req.params.id; | ||||||
|  |     requireFields({ classId }); | ||||||
|  | 
 | ||||||
|  |     const newData = req.body as Partial<EntityDTO<Class>>; | ||||||
|  |     const cls = await putClass(classId, newData); | ||||||
|  | 
 | ||||||
|  |     res.json({ class: cls }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteClassHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const classId = req.params.id; | ||||||
|  |     const cls = await deleteClass(classId); | ||||||
|  | 
 | ||||||
|  |     res.json({ class: cls }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> { | export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classId = req.params.id; |     const classId = req.params.id; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ classId }); | ||||||
| 
 | 
 | ||||||
|     const students = await getClassStudents(classId, full); |     const students = await getClassStudents(classId, full); | ||||||
| 
 | 
 | ||||||
| 	if (!students) { |     res.json({ students }); | ||||||
| 		res.status(404).json({ error: 'Class not found' }); | } | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
|     res.json({ | export async function getClassTeachersHandler(req: Request, res: Response): Promise<void> { | ||||||
|         students: students, |     const classId = req.params.id; | ||||||
|     }); |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ classId }); | ||||||
|  | 
 | ||||||
|  |     const teachers = await getClassTeachers(classId, full); | ||||||
|  | 
 | ||||||
|  |     res.json({ teachers }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> { | export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classId = req.params.id; |     const classId = req.params.id; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ classId }); | ||||||
| 
 | 
 | ||||||
|     const invitations = await getClassTeacherInvitations(classId, full); |     const invitations = await getClassTeacherInvitations(classId, full); | ||||||
| 
 | 
 | ||||||
|     if (!invitations) { |     res.json({ invitations }); | ||||||
|         res.status(404).json({ error: 'Class not found' }); | } | ||||||
|         return; | 
 | ||||||
|     } | export async function deleteClassStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
| 
 |     const classId = req.params.id; | ||||||
|     res.json({ |     const username = req.params.username; | ||||||
|         invitations: invitations, |     requireFields({ classId, username }); | ||||||
|     }); | 
 | ||||||
|  |     const cls = await deleteClassStudent(classId, username); | ||||||
|  | 
 | ||||||
|  |     res.json({ class: cls }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteClassTeacherHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const classId = req.params.id; | ||||||
|  |     const username = req.params.username; | ||||||
|  |     requireFields({ classId, username }); | ||||||
|  | 
 | ||||||
|  |     const cls = await deleteClassTeacher(classId, username); | ||||||
|  | 
 | ||||||
|  |     res.json({ class: cls }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function addClassStudentHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const classId = req.params.id; | ||||||
|  |     const username = req.body.username; | ||||||
|  |     requireFields({ classId, username }); | ||||||
|  | 
 | ||||||
|  |     const cls = await addClassStudent(classId, username); | ||||||
|  | 
 | ||||||
|  |     res.json({ class: cls }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function addClassTeacherHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const classId = req.params.id; | ||||||
|  |     const username = req.body.username; | ||||||
|  |     requireFields({ classId, username }); | ||||||
|  | 
 | ||||||
|  |     const cls = await addClassTeacher(classId, username); | ||||||
|  | 
 | ||||||
|  |     res.json({ class: cls }); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								backend/src/controllers/error-helper.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend/src/controllers/error-helper.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Checks for the presence of required fields and throws a BadRequestException | ||||||
|  |  * if any are missing. | ||||||
|  |  * | ||||||
|  |  * @param fields - An object with key-value pairs to validate. | ||||||
|  |  */ | ||||||
|  | export function requireFields(fields: Record<string, unknown>): void { | ||||||
|  |     const missing = Object.entries(fields) | ||||||
|  |         .filter(([_, value]) => value === undefined || value === null || value === '') | ||||||
|  |         .map(([key]) => key); | ||||||
|  | 
 | ||||||
|  |     if (missing.length > 0) { | ||||||
|  |         const message = `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`; | ||||||
|  |         throw new BadRequestException(message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,93 +1,120 @@ | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; | import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupQuestions, getGroupSubmissions, putGroup } from '../services/groups.js'; | ||||||
| import { GroupDTO } from '../interfaces/group.js'; | import { GroupDTO } from '@dwengo-1/common/interfaces/group'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
|  | 
 | ||||||
|  | function checkGroupFields(classId: string, assignmentId: number, groupId: number): void { | ||||||
|  |     requireFields({ classId, assignmentId, groupId }); | ||||||
|  | 
 | ||||||
|  |     if (isNaN(assignmentId)) { | ||||||
|  |         throw new BadRequestException('Assignment id must be a number'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (isNaN(groupId)) { | ||||||
|  |         throw new BadRequestException('Group id must be a number'); | ||||||
|  |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export async function getGroupHandler(req: Request, res: Response): Promise<void> { | export async function getGroupHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classId = req.params.classid; |     const classId = req.params.classid; | ||||||
|     const full = req.query.full === 'true'; |     const assignmentId = parseInt(req.params.assignmentid); | ||||||
|     const assignmentId = +req.params.assignmentid; |     const groupId = parseInt(req.params.groupid); | ||||||
|  |     checkGroupFields(classId, assignmentId, groupId); | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentId)) { |     const group = await getGroup(classId, assignmentId, groupId); | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const groupId = +req.params.groupid!; // Can't be undefined
 |     res.json({ group }); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     if (isNaN(groupId)) { | export async function putGroupHandler(req: Request, res: Response): Promise<void> { | ||||||
|         res.status(400).json({ error: 'Group id must be a number' }); |     const classId = req.params.classid; | ||||||
|         return; |     const assignmentId = parseInt(req.params.assignmentid); | ||||||
|     } |     const groupId = parseInt(req.params.groupid); | ||||||
|  |     checkGroupFields(classId, assignmentId, groupId); | ||||||
| 
 | 
 | ||||||
|     const group = await getGroup(classId, assignmentId, groupId, full); |     // Only members field can be changed
 | ||||||
|  |     const members = req.body.members; | ||||||
|  |     requireFields({ members }); | ||||||
| 
 | 
 | ||||||
|     if (!group) { |     const group = await putGroup(classId, assignmentId, groupId, { members } as Partial<GroupDTO>); | ||||||
|         res.status(404).json({ error: 'Group not found' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     res.json(group); |     res.json({ group }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteGroupHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const classId = req.params.classid; | ||||||
|  |     const assignmentId = parseInt(req.params.assignmentid); | ||||||
|  |     const groupId = parseInt(req.params.groupid); | ||||||
|  |     checkGroupFields(classId, assignmentId, groupId); | ||||||
|  | 
 | ||||||
|  |     const group = await deleteGroup(classId, assignmentId, groupId); | ||||||
|  | 
 | ||||||
|  |     res.json({ group }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> { | export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classId = req.params.classid; |     const classId = req.params.classid; | ||||||
|  |     const assignmentId = Number(req.params.assignmentid); | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
| 
 |     requireFields({ classId, assignmentId }); | ||||||
|     const assignmentId = +req.params.assignmentid; |  | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentId)) { |     if (isNaN(assignmentId)) { | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |         throw new BadRequestException('Assignment id must be a number'); | ||||||
|         return; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const groups = await getAllGroups(classId, assignmentId, full); |     const groups = await getAllGroups(classId, assignmentId, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ groups }); | ||||||
|         groups: groups, |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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); | ||||||
|  |     const members = req.body.members; | ||||||
|  |     requireFields({ classid, assignmentId, members }); | ||||||
| 
 | 
 | ||||||
|     if (isNaN(assignmentId)) { |     if (isNaN(assignmentId)) { | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |         throw new BadRequestException('Assignment id must be a number'); | ||||||
|         return; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const groupData = req.body as GroupDTO; |     const groupData = req.body as GroupDTO; | ||||||
|     const group = await createGroup(groupData, classid, assignmentId); |     const group = await createGroup(groupData, classid, assignmentId); | ||||||
| 
 | 
 | ||||||
|     if (!group) { |     res.status(201).json({ group }); | ||||||
|         res.status(500).json({ error: 'Something went wrong while creating group' }); | } | ||||||
|         return; | 
 | ||||||
|  | function getGroupParams(req: Request): { classId: string; assignmentId: number; groupId: number; full: boolean } { | ||||||
|  |     const classId = req.params.classid; | ||||||
|  |     const assignmentId = Number(req.params.assignmentid); | ||||||
|  |     const groupId = Number(req.params.groupid); | ||||||
|  |     const full = req.query.full === 'true'; | ||||||
|  | 
 | ||||||
|  |     requireFields({ classId, assignmentId, groupId }); | ||||||
|  | 
 | ||||||
|  |     if (isNaN(assignmentId)) { | ||||||
|  |         throw new BadRequestException('Assignment id must be a number'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.status(201).json(group); |     if (isNaN(groupId)) { | ||||||
|  |         throw new BadRequestException('Group id must be a number'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { classId, assignmentId, groupId, full }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> { | export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const classId = req.params.classid; |     const { classId, assignmentId, groupId, full } = getGroupParams(req); | ||||||
|     const full = req.query.full === 'true'; |  | ||||||
| 
 |  | ||||||
|     const assignmentId = +req.params.assignmentid; |  | ||||||
| 
 |  | ||||||
|     if (isNaN(assignmentId)) { |  | ||||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const groupId = +req.params.groupid!; // Can't be undefined
 |  | ||||||
| 
 |  | ||||||
|     if (isNaN(groupId)) { |  | ||||||
|         res.status(400).json({ error: 'Group id must be a number' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full); |     const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full); | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ submissions }); | ||||||
|         submissions: submissions, | } | ||||||
|     }); | 
 | ||||||
|  | export async function getGroupQuestionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const { classId, assignmentId, groupId, full } = getGroupParams(req); | ||||||
|  | 
 | ||||||
|  |     const questions = await getGroupQuestions(classId, assignmentId, groupId, full); | ||||||
|  | 
 | ||||||
|  |     res.json({ questions }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 { BadRequestException } from '../exceptions.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 { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
|  | import { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
|  | import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| 
 | 
 | ||||||
| function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { | ||||||
|     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); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,13 +2,11 @@ import { Request, Response } from 'express'; | ||||||
| import { themes } from '../data/themes.js'; | import { themes } from '../data/themes.js'; | ||||||
| import { FALLBACK_LANG } from '../config.js'; | import { FALLBACK_LANG } from '../config.js'; | ||||||
| import learningPathService from '../services/learning-paths/learning-path-service.js'; | import learningPathService from '../services/learning-paths/learning-path-service.js'; | ||||||
| import { BadRequestException, NotFoundException } from '../exceptions.js'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
| import { Language } from '../entities/content/language.js'; | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
| import { | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
|     PersonalizationTarget, | import { Group } from '../entities/assignments/group.entity.js'; | ||||||
|     personalizedForGroup, | import { getAssignmentRepository, getGroupRepository } from '../data/repositories.js'; | ||||||
|     personalizedForStudent, |  | ||||||
| } from '../services/learning-paths/learning-path-personalization-util.js'; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Fetch learning paths based on query parameters. |  * Fetch learning paths based on query parameters. | ||||||
|  | @ -19,20 +17,20 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi | ||||||
|     const searchQuery = req.query.search as string; |     const searchQuery = req.query.search as string; | ||||||
|     const language = (req.query.language as string) || FALLBACK_LANG; |     const language = (req.query.language as string) || FALLBACK_LANG; | ||||||
| 
 | 
 | ||||||
|     const forStudent = req.query.forStudent as string; |  | ||||||
|     const forGroupNo = req.query.forGroup as string; |     const forGroupNo = req.query.forGroup as string; | ||||||
|     const assignmentNo = req.query.assignmentNo as string; |     const assignmentNo = req.query.assignmentNo as string; | ||||||
|     const classId = req.query.classId as string; |     const classId = req.query.classId as string; | ||||||
| 
 | 
 | ||||||
|     let personalizationTarget: PersonalizationTarget | undefined; |     let forGroup: Group | undefined; | ||||||
| 
 | 
 | ||||||
|     if (forStudent) { |     if (forGroupNo) { | ||||||
|         personalizationTarget = await personalizedForStudent(forStudent); |  | ||||||
|     } else if (forGroupNo) { |  | ||||||
|         if (!assignmentNo || !classId) { |         if (!assignmentNo || !classId) { | ||||||
|             throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.'); |             throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.'); | ||||||
|         } |         } | ||||||
|         personalizationTarget = await personalizedForGroup(classId, parseInt(assignmentNo), parseInt(forGroupNo)); |         const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo)); | ||||||
|  |         if (assignment) { | ||||||
|  |             forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let hruidList; |     let hruidList; | ||||||
|  | @ -47,18 +45,13 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi | ||||||
|             throw new NotFoundException(`Theme "${themeKey}" not found.`); |             throw new NotFoundException(`Theme "${themeKey}" not found.`); | ||||||
|         } |         } | ||||||
|     } else if (searchQuery) { |     } else if (searchQuery) { | ||||||
|         const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, personalizationTarget); |         const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup); | ||||||
|         res.json(searchResults); |         res.json(searchResults); | ||||||
|         return; |         return; | ||||||
|     } else { |     } else { | ||||||
|         hruidList = themes.flatMap((theme) => theme.hruids); |         hruidList = themes.flatMap((theme) => theme.hruids); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const learningPaths = await learningPathService.fetchLearningPaths( |     const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup); | ||||||
|         hruidList, |  | ||||||
|         language as Language, |  | ||||||
|         `HRUIDs: ${hruidList.join(', ')}`, |  | ||||||
|         personalizationTarget |  | ||||||
|     ); |  | ||||||
|     res.json(learningPaths.data); |     res.json(learningPaths.data); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,34 +1,27 @@ | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; | import { | ||||||
| import { QuestionDTO, QuestionId } from '../interfaces/question.js'; |     createQuestion, | ||||||
| import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; |     deleteQuestion, | ||||||
|  |     getAllQuestions, | ||||||
|  |     getQuestion, | ||||||
|  |     getQuestionsAboutLearningObjectInAssignment, | ||||||
|  |     updateQuestion, | ||||||
|  | } from '../services/questions.js'; | ||||||
|  | import { FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_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 { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
| 
 | import { Language } from '@dwengo-1/common/util/language'; | ||||||
| function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { | import { requireFields } from './error-helper.js'; | ||||||
|     const { hruid, version } = req.params; |  | ||||||
|     const lang = req.query.lang; |  | ||||||
| 
 |  | ||||||
|     if (!hruid || !version) { |  | ||||||
|         res.status(400).json({ error: 'Missing required parameters.' }); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|  | export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier { | ||||||
|     return { |     return { | ||||||
|         hruid, |         hruid, | ||||||
|         language: (lang as Language) || FALLBACK_LANG, |         language: (lang || FALLBACK_LANG) as Language, | ||||||
|         version: +version, |         version: Number(version) || FALLBACK_VERSION_NUM, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getQuestionId(req: Request, res: Response): QuestionId | null { | export function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId { | ||||||
|     const seq = req.params.seq; |  | ||||||
|     const learningObjectIdentifier = getObjectId(req, res); |  | ||||||
| 
 |  | ||||||
|     if (!learningObjectIdentifier) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return { |     return { | ||||||
|         learningObjectIdentifier, |         learningObjectIdentifier, | ||||||
|         sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, |         sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, | ||||||
|  | @ -36,85 +29,96 @@ function getQuestionId(req: Request, res: Response): QuestionId | null { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> { | export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const objectId = getObjectId(req, res); |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = (req.query.lang ? req.query.lang : FALLBACK_LANG) as string; | ||||||
|     const full = req.query.full === 'true'; |     const full = req.query.full === 'true'; | ||||||
|  |     requireFields({ hruid }); | ||||||
| 
 | 
 | ||||||
|     if (!objectId) { |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const questions = await getAllQuestions(objectId, full); |     let questions: QuestionDTO[] | QuestionId[]; | ||||||
| 
 |     if (req.query.classId && req.query.assignmentId) { | ||||||
|     if (!questions) { |         questions = await getQuestionsAboutLearningObjectInAssignment( | ||||||
|         res.status(404).json({ error: `Questions not found.` }); |             learningObjectId, | ||||||
|  |             req.query.classId as string, | ||||||
|  |             parseInt(req.query.assignmentId as string), | ||||||
|  |             full ?? false, | ||||||
|  |             req.query.forStudent as string | undefined | ||||||
|  |         ); | ||||||
|     } else { |     } else { | ||||||
|         res.json({ questions: questions }); |         questions = await getAllQuestions(learningObjectId, full ?? false); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     res.json({ questions }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getQuestionHandler(req: Request, res: Response): Promise<void> { | export async function getQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const questionId = getQuestionId(req, res); |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
| 
 | 
 | ||||||
|     if (!questionId) { |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|         return; |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const question = await getQuestion(questionId); |     const question = await getQuestion(questionId); | ||||||
| 
 | 
 | ||||||
|     if (!question) { |     res.json({ question }); | ||||||
|         res.status(404).json({ error: `Question not found.` }); |  | ||||||
|     } else { |  | ||||||
|         res.json(question); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export async function getQuestionAnswersHandler(req: Request, res: Response): Promise<void> { |  | ||||||
|     const questionId = getQuestionId(req, res); |  | ||||||
|     const full = req.query.full === 'true'; |  | ||||||
| 
 |  | ||||||
|     if (!questionId) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const answers = await getAnswersByQuestion(questionId, full); |  | ||||||
| 
 |  | ||||||
|     if (!answers) { |  | ||||||
|         res.status(404).json({ error: `Questions not found` }); |  | ||||||
|     } else { |  | ||||||
|         res.json({ answers: answers }); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createQuestionHandler(req: Request, res: Response): Promise<void> { | export async function createQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const questionDTO = req.body as QuestionDTO; |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     requireFields({ hruid }); | ||||||
| 
 | 
 | ||||||
|     if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) { |     const loId = getLearningObjectId(hruid, version, language); | ||||||
|         res.status(400).json({ error: 'Missing required fields: identifier and content' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const question = await createQuestion(questionDTO); |     const author = req.body.author as string; | ||||||
|  |     const content = req.body.content as string; | ||||||
|  |     const inGroup = req.body.inGroup; | ||||||
|  |     requireFields({ author, content, inGroup }); | ||||||
| 
 | 
 | ||||||
|     if (!question) { |     const questionData = req.body as QuestionData; | ||||||
|         res.status(400).json({ error: 'Could not create question' }); | 
 | ||||||
|     } else { |     const question = await createQuestion(loId, questionData); | ||||||
|         res.json(question); | 
 | ||||||
|     } |     res.json({ question }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> { | export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const questionId = getQuestionId(req, res); |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
| 
 | 
 | ||||||
|     if (!questionId) { |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|         res.json(404).json({ error: 'Question not found' }); |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const question = await deleteQuestion(questionId); |     const question = await deleteQuestion(questionId); | ||||||
| 
 | 
 | ||||||
|     if (!question) { |     res.json({ question }); | ||||||
|         res.status(404).json({ error: 'Could not find nor delete question' }); | } | ||||||
|     } else { | 
 | ||||||
|         res.json(question); | export async function updateQuestionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     } |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const content = req.body.content as string; | ||||||
|  |     requireFields({ content }); | ||||||
|  | 
 | ||||||
|  |     const questionData = req.body as QuestionData; | ||||||
|  | 
 | ||||||
|  |     const question = await updateQuestion(questionId, questionData); | ||||||
|  | 
 | ||||||
|  |     res.json({ question }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 = await 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 }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,68 +1,86 @@ | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| import { createSubmission, deleteSubmission, getAllSubmissions, getSubmission } from '../services/submissions.js'; | import { | ||||||
| import { Language, languageMap } from '../entities/content/language.js'; |     createSubmission, | ||||||
| import { SubmissionDTO } from '../interfaces/submission'; |     deleteSubmission, | ||||||
|  |     getAllSubmissions, | ||||||
|  |     getSubmission, | ||||||
|  |     getSubmissionsForLearningObjectAndAssignment, | ||||||
|  | } from '../services/submissions.js'; | ||||||
|  | import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; | ||||||
|  | import { Language, languageMap } from '@dwengo-1/common/util/language'; | ||||||
|  | import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
|  | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
|  | 
 | ||||||
|  | export async function getSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const loHruid = req.params.hruid; | ||||||
|  |     const lang = languageMap[req.query.language as string] || Language.Dutch; | ||||||
|  |     const version = parseInt(req.query.version as string) ?? 1; | ||||||
|  | 
 | ||||||
|  |     const forGroup = req.query.forGroup as string | undefined; | ||||||
|  | 
 | ||||||
|  |     const submissions: SubmissionDTO[] = await getSubmissionsForLearningObjectAndAssignment( | ||||||
|  |         loHruid, | ||||||
|  |         lang, | ||||||
|  |         version, | ||||||
|  |         req.query.classId as string, | ||||||
|  |         parseInt(req.query.assignmentId as string), | ||||||
|  |         forGroup ? parseInt(forGroup) : undefined | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     res.json({ submissions }); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export async function getSubmissionHandler(req: Request, res: Response): Promise<void> { | export async function getSubmissionHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const lohruid = req.params.hruid; |     const lohruid = req.params.hruid; | ||||||
|     const submissionNumber = +req.params.id; |  | ||||||
| 
 |  | ||||||
|     if (isNaN(submissionNumber)) { |  | ||||||
|         res.status(400).json({ error: 'Submission number is not a number' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     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; | ||||||
|  |     const submissionNumber = Number(req.params.id); | ||||||
|  |     requireFields({ lohruid, submissionNumber }); | ||||||
| 
 | 
 | ||||||
|     const submission = await getSubmission(lohruid, lang, version, submissionNumber); |     if (isNaN(submissionNumber)) { | ||||||
| 
 |         throw new BadRequestException('Submission number must be a number'); | ||||||
|     if (!submission) { |  | ||||||
|         res.status(404).json({ error: 'Submission not found' }); |  | ||||||
|         return; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json(submission); |     const loId = new LearningObjectIdentifier(lohruid, lang, version); | ||||||
|  |     const submission = await getSubmission(loId, submissionNumber); | ||||||
|  | 
 | ||||||
|  |     res.json({ submission }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getAllSubmissionsHandler(req: Request, res: Response): Promise<void> { | export async function getAllSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const lohruid = req.params.hruid; |     const lohruid = req.params.hruid; | ||||||
| 
 |  | ||||||
|     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; | ||||||
|  |     requireFields({ lohruid }); | ||||||
| 
 | 
 | ||||||
|     const submissions = await getAllSubmissions(lohruid, lang, version); |     const loId = new LearningObjectIdentifier(lohruid, lang, version); | ||||||
|  |     const submissions = await getAllSubmissions(loId); | ||||||
| 
 | 
 | ||||||
|     res.json({ submissions: submissions }); |     res.json({ submissions }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createSubmissionHandler(req: Request, res: Response) { | // TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
 | ||||||
|  | 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); | ||||||
| 
 | 
 | ||||||
|     if (!submission) { |     res.json({ submission }); | ||||||
|         res.status(400).json({ error: 'Failed to create submission' }); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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 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; | ||||||
|  |     const submissionNumber = Number(req.params.id); | ||||||
|  |     requireFields({ hruid, submissionNumber }); | ||||||
| 
 | 
 | ||||||
|     const submission = await deleteSubmission(hruid, lang, version, submissionNumber); |     if (isNaN(submissionNumber)) { | ||||||
| 
 |         throw new BadRequestException('Submission number must be a number'); | ||||||
|     if (!submission) { |  | ||||||
|         res.status(404).json({ error: 'Submission not found' }); |  | ||||||
|         return; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json(submission); |     const loId = new LearningObjectIdentifier(hruid, lang, version); | ||||||
|  |     const submission = await deleteSubmission(loId, submissionNumber); | ||||||
|  | 
 | ||||||
|  |     res.json({ submission }); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										66
									
								
								backend/src/controllers/teacher-invitations.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								backend/src/controllers/teacher-invitations.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { requireFields } from './error-helper.js'; | ||||||
|  | import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js'; | ||||||
|  | import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; | ||||||
|  | 
 | ||||||
|  | export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const username = req.params.username; | ||||||
|  |     const by = req.query.sent === 'true'; | ||||||
|  |     requireFields({ username }); | ||||||
|  | 
 | ||||||
|  |     const invitations = await getAllInvitations(username, by); | ||||||
|  | 
 | ||||||
|  |     res.json({ invitations }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getInvitationHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const sender = req.params.sender; | ||||||
|  |     const receiver = req.params.receiver; | ||||||
|  |     const classId = req.params.classId; | ||||||
|  |     requireFields({ sender, receiver, classId }); | ||||||
|  | 
 | ||||||
|  |     const invitation = await getInvitation(sender, receiver, classId); | ||||||
|  | 
 | ||||||
|  |     res.json({ invitation }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createInvitationHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const sender = req.body.sender; | ||||||
|  |     const receiver = req.body.receiver; | ||||||
|  |     const classId = req.body.class; | ||||||
|  |     requireFields({ sender, receiver, classId }); | ||||||
|  | 
 | ||||||
|  |     const data = req.body as TeacherInvitationData; | ||||||
|  |     const invitation = await createInvitation(data); | ||||||
|  | 
 | ||||||
|  |     res.json({ invitation }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function updateInvitationHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const sender = req.body.sender; | ||||||
|  |     const receiver = req.body.receiver; | ||||||
|  |     const classId = req.body.class; | ||||||
|  |     req.body.accepted = req.body.accepted !== false; | ||||||
|  |     requireFields({ sender, receiver, classId }); | ||||||
|  | 
 | ||||||
|  |     const data = req.body as TeacherInvitationData; | ||||||
|  |     const invitation = await updateInvitation(data); | ||||||
|  | 
 | ||||||
|  |     res.json({ invitation }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteInvitationHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |     const sender = req.params.sender; | ||||||
|  |     const receiver = req.params.receiver; | ||||||
|  |     const classId = req.params.classId; | ||||||
|  |     requireFields({ sender, receiver, classId }); | ||||||
|  | 
 | ||||||
|  |     const data: TeacherInvitationData = { | ||||||
|  |         sender, | ||||||
|  |         receiver, | ||||||
|  |         class: classId, | ||||||
|  |     }; | ||||||
|  |     const invitation = await deleteInvitation(data); | ||||||
|  | 
 | ||||||
|  |     res.json({ invitation }); | ||||||
|  | } | ||||||
|  | @ -4,137 +4,96 @@ 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 classId = req.params.classId; | ||||||
|     } |     requireFields({ 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.params.studentUsername; | ||||||
|  |     const classId = req.params.classId; | ||||||
|  |     const accepted = req.body.accepted !== 'false'; // Default = true
 | ||||||
|  |     requireFields({ studentUsername, classId }); | ||||||
|  | 
 | ||||||
|  |     const request = await updateClassJoinRequestStatus(studentUsername, classId, accepted); | ||||||
|  |     res.json({ request }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,25 +3,23 @@ import { themes } from '../data/themes.js'; | ||||||
| import { loadTranslations } from '../util/translation-helper.js'; | import { loadTranslations } from '../util/translation-helper.js'; | ||||||
| 
 | 
 | ||||||
| interface Translations { | interface Translations { | ||||||
|     curricula_page: { |     curricula_page: Record<string, { title: string; description?: string }>; | ||||||
|         [key: string]: { title: string; description?: string }; |  | ||||||
|     }; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getThemesHandler(req: Request, res: Response) { | export function getThemesHandler(req: Request, res: Response): void { | ||||||
|     const language = (req.query.language as string)?.toLowerCase() || 'nl'; |     const language = ((req.query.language as string) || 'nl').toLowerCase(); | ||||||
|     const translations = loadTranslations<Translations>(language); |     const translations = loadTranslations<Translations>(language); | ||||||
|     const themeList = themes.map((theme) => ({ |     const themeList = themes.map((theme) => ({ | ||||||
|         key: theme.title, |         key: theme.title, | ||||||
|         title: translations.curricula_page[theme.title]?.title || theme.title, |         title: translations.curricula_page[theme.title].title || theme.title, | ||||||
|         description: translations.curricula_page[theme.title]?.description, |         description: translations.curricula_page[theme.title].description, | ||||||
|         image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, |         image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, | ||||||
|     })); |     })); | ||||||
| 
 | 
 | ||||||
|     res.json(themeList); |     res.json(themeList); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getHruidsByThemeHandler(req: Request, res: Response) { | export function getHruidsByThemeHandler(req: Request, res: Response): void { | ||||||
|     const themeKey = req.params.theme; |     const themeKey = req.params.theme; | ||||||
| 
 | 
 | ||||||
|     if (!themeKey) { |     if (!themeKey) { | ||||||
|  |  | ||||||
|  | @ -3,13 +3,29 @@ 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 }, { populate: ['groups', 'groups.members'] }); | ||||||
|     } |     } | ||||||
|     public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { |     public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise<Assignment | null> { | ||||||
|         return this.findAll({ where: { within: within } }); |         return this.findOne({ within: { classId: withinClass }, id: id }); | ||||||
|     } |     } | ||||||
|     public deleteByClassAndId(within: Class, id: number): Promise<void> { |     public async findAllByResponsibleTeacher(teacherUsername: string): Promise<Assignment[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { | ||||||
|  |                 within: { | ||||||
|  |                     teachers: { | ||||||
|  |                         $some: { | ||||||
|  |                             username: teacherUsername, | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { | ||||||
|  |         return this.findAll({ where: { within: within }, populate: ['groups', 'groups.members'] }); | ||||||
|  |     } | ||||||
|  |     public async deleteByClassAndId(within: Class, id: number): Promise<void> { | ||||||
|         return this.deleteWhere({ within: within, id: id }); |         return this.deleteWhere({ within: within, id: id }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class GroupRepository extends DwengoEntityRepository<Group> { | export class GroupRepository extends DwengoEntityRepository<Group> { | ||||||
|     public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { |     public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { | ||||||
|         return this.findOne( |         return this.findOne( | ||||||
|             { |             { | ||||||
|                 assignment: assignment, |                 assignment: assignment, | ||||||
|  | @ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> { | ||||||
|             { populate: ['members'] } |             { populate: ['members'] } | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|     public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { |     public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { | ||||||
|         return this.findAll({ |         return this.findAll({ | ||||||
|             where: { assignment: assignment }, |             where: { assignment: assignment }, | ||||||
|             populate: ['members'], |             populate: ['members'], | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     public findAllGroupsWithStudent(student: Student): Promise<Group[]> { |     public async findAllGroupsWithStudent(student: Student): Promise<Group[]> { | ||||||
|         return this.find({ members: student }, { populate: ['members'] }); |         return this.find({ members: student }, { populate: ['members'] }); | ||||||
|     } |     } | ||||||
|     public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { |     public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<void> { | ||||||
|         return this.deleteWhere({ |         return this.deleteWhere({ | ||||||
|             assignment: assignment, |             assignment: assignment, | ||||||
|             groupNumber: groupNumber, |             groupNumber: groupNumber, | ||||||
|  |  | ||||||
|  | @ -3,29 +3,30 @@ import { Group } from '../../entities/assignments/group.entity.js'; | ||||||
| import { Submission } from '../../entities/assignments/submission.entity.js'; | import { Submission } from '../../entities/assignments/submission.entity.js'; | ||||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
|  | import { Assignment } from '../../entities/assignments/assignment.entity'; | ||||||
| 
 | 
 | ||||||
| export class SubmissionRepository extends DwengoEntityRepository<Submission> { | export class SubmissionRepository extends DwengoEntityRepository<Submission> { | ||||||
|     public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> { |     public async findSubmissionByLearningObjectAndSubmissionNumber( | ||||||
|         return this.findOne( |         loId: LearningObjectIdentifier, | ||||||
|             { |         submissionNumber: number | ||||||
|                 learningObjectHruid: loId.hruid, |     ): Promise<Submission | null> { | ||||||
|                 learningObjectLanguage: loId.language, |         return this.findOne({ | ||||||
|                 learningObjectVersion: loId.version, |             learningObjectHruid: loId.hruid, | ||||||
|                 submissionNumber: submissionNumber, |             learningObjectLanguage: loId.language, | ||||||
|             }, |             learningObjectVersion: loId.version, | ||||||
|             { populate: ['submitter', 'onBehalfOf'] }, |             submissionNumber: submissionNumber, | ||||||
|         );   |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findSubmissionsByLearningObject(loId: LearningObjectIdentifier): Promise<Submission[]> { |     public async findByLearningObject(loId: LearningObjectIdentifier): Promise<Submission[]> { | ||||||
|         return this.find({ |         return this.find({ | ||||||
|             learningObjectHruid: loId.hruid, |             learningObjectHruid: loId.hruid, | ||||||
|             learningObjectLanguage: loId.language, |             learningObjectLanguage: loId.language, | ||||||
|             learningObjectVersion: loId.version, |             learningObjectVersion: loId.version, | ||||||
|         }) |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     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, | ||||||
|  | @ -37,7 +38,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, | ||||||
|  | @ -49,15 +50,60 @@ 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 }, | ||||||
|  |             { | ||||||
|  |                 populate: ['onBehalfOf.members'], | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { |     /** | ||||||
|         return this.find({ submitter: student }); |      * Looks up all submissions for the given learning object which were submitted as part of the given assignment. | ||||||
|  |      */ | ||||||
|  |     public async findAllSubmissionsForLearningObjectAndAssignment(loId: LearningObjectIdentifier, assignment: Assignment): Promise<Submission[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { | ||||||
|  |                 learningObjectHruid: loId.hruid, | ||||||
|  |                 learningObjectLanguage: loId.language, | ||||||
|  |                 learningObjectVersion: loId.version, | ||||||
|  |                 onBehalfOf: { | ||||||
|  |                     assignment, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> { |     /** | ||||||
|  |      * Looks up all submissions for the given learning object which were submitted by the given group | ||||||
|  |      */ | ||||||
|  |     public async findAllSubmissionsForLearningObjectAndGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { | ||||||
|  |                 learningObjectHruid: loId.hruid, | ||||||
|  |                 learningObjectLanguage: loId.language, | ||||||
|  |                 learningObjectVersion: loId.version, | ||||||
|  |                 onBehalfOf: group, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> { | ||||||
|  |         const result = await this.find( | ||||||
|  |             { submitter: student }, | ||||||
|  |             { | ||||||
|  |                 populate: ['onBehalfOf.members'], | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         // Workaround: For some reason, without this MikroORM generates an UPDATE query with a syntax error in some tests
 | ||||||
|  |         this.em.clear(); | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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, | ||||||
|  |  | ||||||
|  | @ -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 { ClassStatus } 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: ClassStatus.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 }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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'] }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,22 +2,30 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| import { Class } from '../../entities/classes/class.entity.js'; | import { Class } from '../../entities/classes/class.entity.js'; | ||||||
| import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js'; | import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js'; | ||||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
|  | import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | ||||||
| 
 | 
 | ||||||
| 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, status: ClassStatus.Open } }); | ||||||
|     } |     } | ||||||
|     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, | ||||||
|             class: clazz, |             class: clazz, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     public async findBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<TeacherInvitation | null> { | ||||||
|  |         return this.findOne({ | ||||||
|  |             sender: sender, | ||||||
|  |             receiver: receiver, | ||||||
|  |             class: clazz, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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: { | ||||||
|  |  | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
|  | @ -1,9 +1,13 @@ | ||||||
| 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'; | ||||||
|  | import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; | ||||||
|  | import { RequiredEntityData } from '@mikro-orm/core'; | ||||||
|  | import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | ||||||
|  | import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception.js'; | ||||||
| 
 | 
 | ||||||
| export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { | ||||||
|     public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { |     public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { | ||||||
|         return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); |         return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -23,4 +27,27 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | ||||||
|             populate: ['nodes', 'nodes.transitions'], |             populate: ['nodes', 'nodes.transitions'], | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode { | ||||||
|  |         return this.em.create(LearningPathNode, nodeData); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public createTransition(transitionData: RequiredEntityData<LearningPathTransition>): LearningPathTransition { | ||||||
|  |         return this.em.create(LearningPathTransition, transitionData); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async saveLearningPathNodesAndTransitions( | ||||||
|  |         path: LearningPath, | ||||||
|  |         nodes: LearningPathNode[], | ||||||
|  |         transitions: LearningPathTransition[], | ||||||
|  |         options?: { preventOverwrite?: boolean } | ||||||
|  |     ): Promise<void> { | ||||||
|  |         if (options?.preventOverwrite && (await this.findOne(path))) { | ||||||
|  |             throw new EntityAlreadyExistsException('A learning path with this hruid/language combination already exists.'); | ||||||
|  |         } | ||||||
|  |         const em = this.getEntityManager(); | ||||||
|  |         await em.persistAndFlush(path); | ||||||
|  |         await Promise.all(nodes.map(async (it) => em.persistAndFlush(it))); | ||||||
|  |         await Promise.all(transitions.map(async (it) => em.persistAndFlush(it))); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,12 +1,14 @@ | ||||||
| import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | ||||||
|  | import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js'; | ||||||
| 
 | 
 | ||||||
| export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | ||||||
|     public async save(entity: T) { |     public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> { | ||||||
|         const em = this.getEntityManager(); |         if (options?.preventOverwrite && (await this.findOne(entity))) { | ||||||
|         em.persist(entity); |             throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); | ||||||
|         await em.flush(); |         } | ||||||
|  |         await this.getEntityManager().persistAndFlush(entity); | ||||||
|     } |     } | ||||||
|     public async deleteWhere(query: FilterQuery<T>) { |     public async deleteWhere(query: FilterQuery<T>): Promise<void> { | ||||||
|         const toDelete = await this.findOne(query); |         const toDelete = await this.findOne(query); | ||||||
|         const em = this.getEntityManager(); |         const em = this.getEntityManager(); | ||||||
|         if (toDelete) { |         if (toDelete) { | ||||||
|  |  | ||||||
|  | @ -2,27 +2,43 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| import { Answer } from '../../entities/questions/answer.entity.js'; | import { Answer } from '../../entities/questions/answer.entity.js'; | ||||||
| import { Question } from '../../entities/questions/question.entity.js'; | import { Question } from '../../entities/questions/question.entity.js'; | ||||||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
|  | import { Loaded } from '@mikro-orm/core'; | ||||||
| 
 | 
 | ||||||
| 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, | ||||||
|             content: answer.content, |             content: answer.content, | ||||||
|             timestamp: new Date(), |             timestamp: new Date(), | ||||||
|         }); |         }); | ||||||
|         return this.insert(answerEntity); |         await this.insert(answerEntity); | ||||||
|  |         answerEntity.toQuestion = answer.toQuestion; | ||||||
|  |         answerEntity.author = answer.author; | ||||||
|  |         answerEntity.content = answer.content; | ||||||
|  |         return 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 findAnswer(question: Question, sequenceNumber: number): Promise<Loaded<Answer> | null> { | ||||||
|  |         return this.findOne({ | ||||||
|  |             toQuestion: question, | ||||||
|  |             sequenceNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { | ||||||
|         return this.deleteWhere({ |         return this.deleteWhere({ | ||||||
|             toQuestion: question, |             toQuestion: question, | ||||||
|             sequenceNumber: sequenceNumber, |             sequenceNumber: sequenceNumber, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     public async updateContent(answer: Answer, newContent: string): Promise<Answer> { | ||||||
|  |         answer.content = newContent; | ||||||
|  |         await this.save(answer); | ||||||
|  |         return answer; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,14 +3,18 @@ import { Question } from '../../entities/questions/question.entity.js'; | ||||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | 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'; | ||||||
|  | import { Assignment } from '../../entities/assignments/assignment.entity.js'; | ||||||
|  | import { Loaded } from '@mikro-orm/core'; | ||||||
|  | import { Group } from '../../entities/assignments/group.entity'; | ||||||
| 
 | 
 | ||||||
| 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; inGroup: Group; 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, | ||||||
|             learningObjectVersion: question.loId.version, |             learningObjectVersion: question.loId.version, | ||||||
|             author: question.author, |             author: question.author, | ||||||
|  |             inGroup: question.inGroup, | ||||||
|             content: question.content, |             content: question.content, | ||||||
|             timestamp: new Date(), |             timestamp: new Date(), | ||||||
|         }); |         }); | ||||||
|  | @ -18,10 +22,11 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|         questionEntity.learningObjectLanguage = question.loId.language; |         questionEntity.learningObjectLanguage = question.loId.language; | ||||||
|         questionEntity.learningObjectVersion = question.loId.version; |         questionEntity.learningObjectVersion = question.loId.version; | ||||||
|         questionEntity.author = question.author; |         questionEntity.author = question.author; | ||||||
|  |         questionEntity.inGroup = question.inGroup; | ||||||
|         questionEntity.content = question.content; |         questionEntity.content = question.content; | ||||||
|         return this.insert(questionEntity); |         return await 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 +38,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 +59,73 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|             orderBy: { timestamp: 'ASC' }, |             orderBy: { timestamp: 'ASC' }, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public async findAllByAssignment(assignment: Assignment): Promise<Question[]> { | ||||||
|  |         return this.find({ | ||||||
|  |             inGroup: assignment.groups.getItems(), | ||||||
|  |             learningObjectHruid: assignment.learningPathHruid, | ||||||
|  |             learningObjectLanguage: assignment.learningPathLanguage, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async findAllByAuthor(author: Student): Promise<Question[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { author }, | ||||||
|  |             orderBy: { timestamp: 'DESC' }, // New to old
 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async findAllByGroup(inGroup: Group): Promise<Question[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { inGroup }, | ||||||
|  |             orderBy: { timestamp: 'DESC' }, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Looks up all questions for the given learning object which were asked as part of the given assignment. | ||||||
|  |      * When forStudentUsername is set, only the questions within the given user's group are shown. | ||||||
|  |      */ | ||||||
|  |     public async findAllQuestionsAboutLearningObjectInAssignment( | ||||||
|  |         loId: LearningObjectIdentifier, | ||||||
|  |         assignment: Assignment, | ||||||
|  |         forStudentUsername?: string | ||||||
|  |     ): Promise<Question[]> { | ||||||
|  |         const inGroup = forStudentUsername | ||||||
|  |             ? { | ||||||
|  |                   assignment, | ||||||
|  |                   members: { | ||||||
|  |                       $some: { | ||||||
|  |                           username: forStudentUsername, | ||||||
|  |                       }, | ||||||
|  |                   }, | ||||||
|  |               } | ||||||
|  |             : { | ||||||
|  |                   assignment, | ||||||
|  |               }; | ||||||
|  | 
 | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { | ||||||
|  |                 learningObjectHruid: loId.hruid, | ||||||
|  |                 learningObjectLanguage: loId.language, | ||||||
|  |                 learningObjectVersion: loId.version, | ||||||
|  |                 inGroup, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<Loaded<Question> | null> { | ||||||
|  |         return this.findOne({ | ||||||
|  |             learningObjectHruid: loId.hruid, | ||||||
|  |             learningObjectLanguage: loId.language, | ||||||
|  |             learningObjectVersion: loId.version, | ||||||
|  |             sequenceNumber, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async updateContent(question: Question, newContent: string): Promise<Question> { | ||||||
|  |         question.content = newContent; | ||||||
|  |         await this.save(question); | ||||||
|  |         return question; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -34,8 +34,8 @@ let entityManager: EntityManager | undefined; | ||||||
| /** | /** | ||||||
|  * Execute all the database operations within the function f in a single transaction. |  * Execute all the database operations within the function f in a single transaction. | ||||||
|  */ |  */ | ||||||
| export function transactional<T>(f: () => Promise<T>) { | export async function transactional<T>(f: () => Promise<T>): Promise<void> { | ||||||
|     entityManager?.transactional(f); |     await entityManager?.transactional(f); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R { | function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R { | ||||||
|  |  | ||||||
|  | @ -1,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[] = [ | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -1,18 +1,11 @@ | ||||||
| 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 { 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 findByClass(cls: Class): Promise<Student[]> { |     public async deleteByUsername(username: string): Promise<void> { | ||||||
|         return this.find({ classes: cls }); |  | ||||||
|     } |  | ||||||
|     public deleteByUsername(username: string): Promise<void> { |  | ||||||
|         return this.deleteWhere({ username: username }); |         return this.deleteWhere({ username: username }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,10 +2,10 @@ import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| 
 | 
 | ||||||
| export class TeacherRepository extends DwengoEntityRepository<Teacher> { | export class TeacherRepository extends DwengoEntityRepository<Teacher> { | ||||||
|     public findByUsername(username: string): Promise<Teacher | null> { |     public async findByUsername(username: string): Promise<Teacher | null> { | ||||||
|         return this.findOne({ username: username }); |         return this.findOne({ username: username }); | ||||||
|     } |     } | ||||||
|     public deleteByUsername(username: string): Promise<void> { |     public async deleteByUsername(username: string): Promise<void> { | ||||||
|         return this.deleteWhere({ username: username }); |         return this.deleteWhere({ username: username }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||||
| import { User } from '../../entities/users/user.entity.js'; | import { User } from '../../entities/users/user.entity.js'; | ||||||
| 
 | 
 | ||||||
| export class UserRepository<T extends User> extends DwengoEntityRepository<T> { | export class UserRepository<T extends User> extends DwengoEntityRepository<T> { | ||||||
|     public findByUsername(username: string): Promise<T | null> { |     public async findByUsername(username: string): Promise<T | null> { | ||||||
|         return this.findOne({ username } as Partial<T>); |         return this.findOne({ username } as Partial<T>); | ||||||
|     } |     } | ||||||
|     public deleteByUsername(username: string): Promise<void> { |     public async deleteByUsername(username: string): Promise<void> { | ||||||
|         return this.deleteWhere({ username } as Partial<T>); |         return this.deleteWhere({ username } as Partial<T>); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | import { Cascade, Collection, 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({ | ||||||
|  | @ -14,7 +14,7 @@ export class Assignment { | ||||||
|     }) |     }) | ||||||
|     within!: Class; |     within!: Class; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'number', autoincrement: true }) |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|     id?: number; |     id?: number; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|  | @ -34,6 +34,7 @@ export class Assignment { | ||||||
|     @OneToMany({ |     @OneToMany({ | ||||||
|         entity: () => Group, |         entity: () => Group, | ||||||
|         mappedBy: 'assignment', |         mappedBy: 'assignment', | ||||||
|  |         cascade: [Cascade.ALL], | ||||||
|     }) |     }) | ||||||
|     groups!: Group[]; |     groups: Collection<Group> = new Collection<Group>(this); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; | import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; | ||||||
| import { Assignment } from './assignment.entity.js'; | import { Assignment } from './assignment.entity.js'; | ||||||
| import { Student } from '../users/student.entity.js'; | import { Student } from '../users/student.entity.js'; | ||||||
| import { GroupRepository } from '../../data/assignments/group-repository.js'; | import { GroupRepository } from '../../data/assignments/group-repository.js'; | ||||||
|  | @ -7,17 +7,23 @@ import { GroupRepository } from '../../data/assignments/group-repository.js'; | ||||||
|     repository: () => GroupRepository, |     repository: () => GroupRepository, | ||||||
| }) | }) | ||||||
| export class Group { | export class Group { | ||||||
|  |     /* | ||||||
|  |      WARNING: Don't move the definition of groupNumber! If it does not come before the definition of assignment, | ||||||
|  |      creating groups fails because of a MikroORM bug! | ||||||
|  |      */ | ||||||
|  |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|  |     groupNumber?: number; | ||||||
|  | 
 | ||||||
|     @ManyToOne({ |     @ManyToOne({ | ||||||
|         entity: () => Assignment, |         entity: () => Assignment, | ||||||
|         primary: true, |         primary: true, | ||||||
|     }) |     }) | ||||||
|     assignment!: Assignment; |     assignment!: Assignment; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) |  | ||||||
|     groupNumber?: number; |  | ||||||
| 
 |  | ||||||
|     @ManyToMany({ |     @ManyToMany({ | ||||||
|         entity: () => Student, |         entity: () => Student, | ||||||
|  |         owner: true, | ||||||
|  |         inversedBy: 'groups', | ||||||
|     }) |     }) | ||||||
|     members!: Student[]; |     members: Collection<Student> = new Collection<Student>(this); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| 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, Cascade } 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 { | ||||||
|  |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|  |     submissionNumber?: number; | ||||||
|  | 
 | ||||||
|     @PrimaryKey({ type: 'string' }) |     @PrimaryKey({ type: 'string' }) | ||||||
|     learningObjectHruid!: string; |     learningObjectHruid!: string; | ||||||
| 
 | 
 | ||||||
|  | @ -15,11 +18,13 @@ export class Submission { | ||||||
|     }) |     }) | ||||||
|     learningObjectLanguage!: Language; |     learningObjectLanguage!: Language; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'numeric' }) |     @PrimaryKey({ type: 'numeric', autoincrement: false }) | ||||||
|     learningObjectVersion: number = 1; |     learningObjectVersion = 1; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) |     @ManyToOne(() => Group, { | ||||||
|     submissionNumber?: number; |         cascade: [Cascade.REMOVE], | ||||||
|  |     }) | ||||||
|  |     onBehalfOf!: Group; | ||||||
| 
 | 
 | ||||||
|     @ManyToOne({ |     @ManyToOne({ | ||||||
|         entity: () => Student, |         entity: () => Student, | ||||||
|  | @ -29,12 +34,6 @@ export class Submission { | ||||||
|     @Property({ type: 'datetime' }) |     @Property({ type: 'datetime' }) | ||||||
|     submissionTime!: Date; |     submissionTime!: Date; | ||||||
| 
 | 
 | ||||||
|     @ManyToOne({ |  | ||||||
|         entity: () => Group, |  | ||||||
|         nullable: true, |  | ||||||
|     }) |  | ||||||
|     onBehalfOf?: Group; |  | ||||||
| 
 |  | ||||||
|     @Property({ type: 'json' }) |     @Property({ type: 'json' }) | ||||||
|     content!: string; |     content!: string; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | ||||||
| export enum ClassJoinRequestStatus { |  | ||||||
|     Open = 'open', |  | ||||||
|     Accepted = 'accepted', |  | ||||||
|     Declined = 'declined', |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| @Entity({ | @Entity({ | ||||||
|     repository: () => ClassJoinRequestRepository, |     repository: () => ClassJoinRequestRepository, | ||||||
|  | @ -25,6 +20,6 @@ export class ClassJoinRequest { | ||||||
|     }) |     }) | ||||||
|     class!: Class; |     class!: Class; | ||||||
| 
 | 
 | ||||||
|     @Enum(() => ClassJoinRequestStatus) |     @Enum(() => ClassStatus) | ||||||
|     status!: ClassJoinRequestStatus; |     status!: ClassStatus; | ||||||
| } | } | ||||||
|  | @ -1,22 +1,24 @@ | ||||||
| import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core'; | import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||||
| import { v4 } from 'uuid'; |  | ||||||
| import { Teacher } from '../users/teacher.entity.js'; | import { Teacher } from '../users/teacher.entity.js'; | ||||||
| import { Student } from '../users/student.entity.js'; | import { Student } from '../users/student.entity.js'; | ||||||
| import { ClassRepository } from '../../data/classes/class-repository.js'; | import { ClassRepository } from '../../data/classes/class-repository.js'; | ||||||
|  | import { customAlphabet } from 'nanoid'; | ||||||
|  | 
 | ||||||
|  | const generateClassId = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 6); | ||||||
| 
 | 
 | ||||||
| @Entity({ | @Entity({ | ||||||
|     repository: () => ClassRepository, |     repository: () => ClassRepository, | ||||||
| }) | }) | ||||||
| export class Class { | export class Class { | ||||||
|     @PrimaryKey() |     @PrimaryKey() | ||||||
|     classId? = v4(); |     classId? = generateClassId(); | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     displayName!: string; |     displayName!: string; | ||||||
| 
 | 
 | ||||||
|     @ManyToMany(() => Teacher) |     @ManyToMany({ entity: () => Teacher, owner: true, inversedBy: 'classes' }) | ||||||
|     teachers!: Collection<Teacher>; |     teachers!: Collection<Teacher>; | ||||||
| 
 | 
 | ||||||
|     @ManyToMany(() => Student) |     @ManyToMany({ entity: () => Student, owner: true, inversedBy: 'classes' }) | ||||||
|     students!: Collection<Student>; |     students!: Collection<Student>; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| import { Entity, ManyToOne } from '@mikro-orm/core'; | import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; | ||||||
| import { Teacher } from '../users/teacher.entity.js'; | import { Teacher } from '../users/teacher.entity.js'; | ||||||
| import { Class } from './class.entity.js'; | import { Class } from './class.entity.js'; | ||||||
| import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js'; | import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js'; | ||||||
|  | import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Invitation of a teacher into a class (in order to teach it). |  * Invitation of a teacher into a class (in order to teach it). | ||||||
|  | @ -25,4 +26,7 @@ export class TeacherInvitation { | ||||||
|         primary: true, |         primary: true, | ||||||
|     }) |     }) | ||||||
|     class!: Class; |     class!: Class; | ||||||
|  | 
 | ||||||
|  |     @Enum(() => ClassStatus) | ||||||
|  |     status!: ClassStatus; | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								backend/src/entities/content/educational-goal.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/entities/content/educational-goal.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { Embeddable, Property } from '@mikro-orm/core'; | ||||||
|  | 
 | ||||||
|  | @Embeddable() | ||||||
|  | export class EducationalGoal { | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     source!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     id!: string; | ||||||
|  | } | ||||||
|  | @ -1,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
 | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,28 +1,12 @@ | ||||||
| import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | import { ArrayType, 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(); | ||||||
|  | @ -58,11 +42,11 @@ export class LearningObject { | ||||||
|     @Property({ type: 'array' }) |     @Property({ type: 'array' }) | ||||||
|     keywords: string[] = []; |     keywords: string[] = []; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'array', nullable: true }) |     @Property({ type: new ArrayType((i) => Number(i)), nullable: true }) | ||||||
|     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; | ||||||
|  |  | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; | import { Collection, 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 { | ||||||
|  |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|  |     nodeNumber?: number; | ||||||
|  | 
 | ||||||
|     @ManyToOne({ entity: () => LearningPath, primary: true }) |     @ManyToOne({ entity: () => LearningPath, primary: true }) | ||||||
|     learningPath!: Rel<LearningPath>; |     learningPath!: Rel<LearningPath>; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) |  | ||||||
|     nodeNumber!: number; |  | ||||||
| 
 |  | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     learningObjectHruid!: string; |     learningObjectHruid!: string; | ||||||
| 
 | 
 | ||||||
|  | @ -27,7 +27,7 @@ export class LearningPathNode { | ||||||
|     startNode!: boolean; |     startNode!: boolean; | ||||||
| 
 | 
 | ||||||
|     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) |     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) | ||||||
|     transitions: LearningPathTransition[] = []; |     transitions!: Collection<LearningPathTransition>; | ||||||
| 
 | 
 | ||||||
|     @Property({ length: 3 }) |     @Property({ length: 3 }) | ||||||
|     createdAt: Date = new Date(); |     createdAt: Date = new Date(); | ||||||
|  |  | ||||||
|  | @ -3,12 +3,12 @@ import { LearningPathNode } from './learning-path-node.entity.js'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| export class LearningPathTransition { | export class LearningPathTransition { | ||||||
|     @ManyToOne({ entity: () => LearningPathNode, primary: true }) |  | ||||||
|     node!: Rel<LearningPathNode>; |  | ||||||
| 
 |  | ||||||
|     @PrimaryKey({ type: 'numeric' }) |     @PrimaryKey({ type: 'numeric' }) | ||||||
|     transitionNumber!: number; |     transitionNumber!: number; | ||||||
| 
 | 
 | ||||||
|  |     @ManyToOne({ entity: () => LearningPathNode, primary: true }) | ||||||
|  |     node!: Rel<LearningPathNode>; | ||||||
|  | 
 | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     condition!: string; |     condition!: string; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | import { Collection, 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 { | ||||||
|  | @ -25,5 +25,5 @@ export class LearningPath { | ||||||
|     image: Buffer | null = null; |     image: Buffer | null = null; | ||||||
| 
 | 
 | ||||||
|     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) |     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) | ||||||
|     nodes: LearningPathNode[] = []; |     nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								backend/src/entities/content/return-value.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/entities/content/return-value.entity.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { Embeddable, Property } from '@mikro-orm/core'; | ||||||
|  | 
 | ||||||
|  | @Embeddable() | ||||||
|  | export class ReturnValue { | ||||||
|  |     @Property({ type: 'string' }) | ||||||
|  |     callbackUrl!: string; | ||||||
|  | 
 | ||||||
|  |     @Property({ type: 'json' }) | ||||||
|  |     callbackSchema!: string; | ||||||
|  | } | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| 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'; | ||||||
|  | import { Group } from '../assignments/group.entity.js'; | ||||||
| 
 | 
 | ||||||
| @Entity({ repository: () => QuestionRepository }) | @Entity({ repository: () => QuestionRepository }) | ||||||
| export class Question { | export class Question { | ||||||
|  | @ -15,11 +16,14 @@ 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; | ||||||
| 
 | 
 | ||||||
|  |     @ManyToOne({ entity: () => Group }) | ||||||
|  |     inGroup!: Group; | ||||||
|  | 
 | ||||||
|     @ManyToOne({ |     @ManyToOne({ | ||||||
|         entity: () => Student, |         entity: () => Student, | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  | @ -8,17 +8,9 @@ import { StudentRepository } from '../../data/users/student-repository.js'; | ||||||
|     repository: () => StudentRepository, |     repository: () => StudentRepository, | ||||||
| }) | }) | ||||||
| export class Student extends User { | export class Student extends User { | ||||||
|     @ManyToMany(() => Class) |     @ManyToMany({ entity: () => Class, mappedBy: 'students' }) | ||||||
|     classes!: Collection<Class>; |     classes!: Collection<Class>; | ||||||
| 
 | 
 | ||||||
|     @ManyToMany(() => Group) |     @ManyToMany({ entity: () => Group, mappedBy: 'members' }) | ||||||
|     groups!: Collection<Group>; |     groups: Collection<Group> = new Collection<Group>(this); | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         public username: string, |  | ||||||
|         public firstName: string, |  | ||||||
|         public lastName: string |  | ||||||
|     ) { |  | ||||||
|         super(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,14 +5,6 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js'; | ||||||
| 
 | 
 | ||||||
| @Entity({ repository: () => TeacherRepository }) | @Entity({ repository: () => TeacherRepository }) | ||||||
| export class Teacher extends User { | export class Teacher extends User { | ||||||
|     @ManyToMany(() => Class) |     @ManyToMany({ entity: () => Class, mappedBy: 'teachers' }) | ||||||
|     classes!: Collection<Class>; |     classes!: Collection<Class>; | ||||||
| 
 |  | ||||||
|     constructor( |  | ||||||
|         public username: string, |  | ||||||
|         public firstName: string, |  | ||||||
|         public lastName: string |  | ||||||
|     ) { |  | ||||||
|         super(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,8 +6,8 @@ export abstract class User { | ||||||
|     username!: string; |     username!: string; | ||||||
| 
 | 
 | ||||||
|     @Property() |     @Property() | ||||||
|     firstName: string = ''; |     firstName = ''; | ||||||
| 
 | 
 | ||||||
|     @Property() |     @Property() | ||||||
|     lastName: string = ''; |     lastName = ''; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,42 +0,0 @@ | ||||||
| /** |  | ||||||
|  * Exception for HTTP 400 Bad Request |  | ||||||
|  */ |  | ||||||
| export class BadRequestException extends Error { |  | ||||||
|     public status = 400; |  | ||||||
| 
 |  | ||||||
|     constructor(error: string) { |  | ||||||
|         super(error); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 401 Unauthorized |  | ||||||
|  */ |  | ||||||
| export class UnauthorizedException extends Error { |  | ||||||
|     status = 401; |  | ||||||
|     constructor(message: string = 'Unauthorized') { |  | ||||||
|         super(message); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 403 Forbidden |  | ||||||
|  */ |  | ||||||
| export class ForbiddenException extends Error { |  | ||||||
|     status = 403; |  | ||||||
| 
 |  | ||||||
|     constructor(message: string = 'Forbidden') { |  | ||||||
|         super(message); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Exception for HTTP 404 Not Found |  | ||||||
|  */ |  | ||||||
| export class NotFoundException extends Error { |  | ||||||
|     public status = 404; |  | ||||||
| 
 |  | ||||||
|     constructor(error: string) { |  | ||||||
|         super(error); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 400 Bad Request | ||||||
|  |  */ | ||||||
|  | export class BadRequestException extends ExceptionWithHttpState { | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(400, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 409 Conflict | ||||||
|  |  */ | ||||||
|  | export class ConflictException extends ExceptionWithHttpState { | ||||||
|  |     public status = 409; | ||||||
|  | 
 | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(409, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | import { ConflictException } from './conflict-exception.js'; | ||||||
|  | 
 | ||||||
|  | export class EntityAlreadyExistsException extends ConflictException { | ||||||
|  |     constructor(message: string) { | ||||||
|  |         super(message); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | import { HasStatusCode } from './has-status-code'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exceptions which are associated with a HTTP error code. | ||||||
|  |  */ | ||||||
|  | export abstract class ExceptionWithHttpState extends Error implements HasStatusCode { | ||||||
|  |     constructor( | ||||||
|  |         public status: number, | ||||||
|  |         public error: string | ||||||
|  |     ) { | ||||||
|  |         super(error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 403 Forbidden | ||||||
|  |  */ | ||||||
|  | export class ForbiddenException extends ExceptionWithHttpState { | ||||||
|  |     status = 403; | ||||||
|  | 
 | ||||||
|  |     constructor(message = 'Forbidden') { | ||||||
|  |         super(403, message); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								backend/src/exceptions/has-status-code.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								backend/src/exceptions/has-status-code.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | export interface HasStatusCode { | ||||||
|  |     status: number; | ||||||
|  | } | ||||||
|  | export function hasStatusCode(err: unknown): err is HasStatusCode { | ||||||
|  |     return typeof err === 'object' && err !== null && 'status' in err && typeof (err as HasStatusCode)?.status === 'number'; | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 404 Not Found | ||||||
|  |  */ | ||||||
|  | export class NotFoundException extends ExceptionWithHttpState { | ||||||
|  |     public status = 404; | ||||||
|  | 
 | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(404, error); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								backend/src/exceptions/server-error-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/server-error-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 500 Internal Server Error | ||||||
|  |  */ | ||||||
|  | export class ServerErrorException extends ExceptionWithHttpState { | ||||||
|  |     status = 500; | ||||||
|  | 
 | ||||||
|  |     constructor(message = 'Internal server error, something went wrong') { | ||||||
|  |         super(500, message); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Exception for HTTP 401 Unauthorized | ||||||
|  |  */ | ||||||
|  | export class UnauthorizedException extends ExceptionWithHttpState { | ||||||
|  |     constructor(message = 'Unauthorized') { | ||||||
|  |         super(401, message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,21 +1,14 @@ | ||||||
| import { mapToUserDTO, UserDTO } from './user.js'; | import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; | ||||||
| import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } 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 { | import { mapToTeacherDTO } from './teacher.js'; | ||||||
|     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. | ||||||
|  */ |  */ | ||||||
| export function mapToAnswerDTO(answer: Answer): AnswerDTO { | export function mapToAnswerDTO(answer: Answer): AnswerDTO { | ||||||
|     return { |     return { | ||||||
|         author: mapToUserDTO(answer.author), |         author: mapToTeacherDTO(answer.author), | ||||||
|         toQuestion: mapToQuestionDTO(answer.toQuestion), |         toQuestion: mapToQuestionDTO(answer.toQuestion), | ||||||
|         sequenceNumber: answer.sequenceNumber!, |         sequenceNumber: answer.sequenceNumber!, | ||||||
|         timestamp: answer.timestamp.toISOString(), |         timestamp: answer.timestamp.toISOString(), | ||||||
|  | @ -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!, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,50 +1,36 @@ | ||||||
| import { FALLBACK_LANG } from '../config.js'; | import { languageMap } from '@dwengo-1/common/util/language'; | ||||||
| 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 { AssignmentDTO, AssignmentDTOId } from '@dwengo-1/common/interfaces/assignment'; | ||||||
| import { GroupDTO } from './group.js'; | import { mapToGroupDTO } from './group.js'; | ||||||
|  | import { getAssignmentRepository } from '../data/repositories.js'; | ||||||
| 
 | 
 | ||||||
| export interface AssignmentDTO { | export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTOId { | ||||||
|     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 { |  | ||||||
|     return { |     return { | ||||||
|         id: assignment.id!, |         id: assignment.id!, | ||||||
|         class: assignment.within.classId!, |         within: assignment.within.classId!, | ||||||
|         title: assignment.title, |  | ||||||
|         description: assignment.description, |  | ||||||
|         learningPath: assignment.learningPathHruid, |  | ||||||
|         language: assignment.learningPathLanguage, |  | ||||||
|         // Groups: assignment.groups.map(group => group.groupNumber),
 |  | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO { | export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO { | ||||||
|     return { |     return { | ||||||
|         id: assignment.id!, |         id: assignment.id!, | ||||||
|         class: assignment.within.classId!, |         within: assignment.within.classId!, | ||||||
|         title: assignment.title, |         title: assignment.title, | ||||||
|         description: assignment.description, |         description: assignment.description, | ||||||
|         learningPath: assignment.learningPathHruid, |         learningPath: assignment.learningPathHruid, | ||||||
|         language: assignment.learningPathLanguage, |         language: assignment.learningPathLanguage, | ||||||
|         // Groups: assignment.groups.map(mapToGroupDTO),
 |         groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment { | export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment { | ||||||
|     const assignment = new Assignment(); |     return getAssignmentRepository().create({ | ||||||
|     assignment.title = assignmentData.title; |         within: cls, | ||||||
|     assignment.description = assignmentData.description; |         title: assignmentData.title, | ||||||
|     assignment.learningPathHruid = assignmentData.learningPath; |         description: assignmentData.description, | ||||||
|     assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; |         learningPathHruid: assignmentData.learningPath, | ||||||
|     assignment.within = cls; |         learningPathLanguage: languageMap[assignmentData.language], | ||||||
| 
 |         groups: [], | ||||||
|     return assignment; |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  | @ -17,7 +10,6 @@ export function mapToClassDTO(cls: Class): ClassDTO { | ||||||
|         displayName: cls.displayName, |         displayName: cls.displayName, | ||||||
|         teachers: cls.teachers.map((teacher) => teacher.username), |         teachers: cls.teachers.map((teacher) => teacher.username), | ||||||
|         students: cls.students.map((student) => student.username), |         students: cls.students.map((student) => student.username), | ||||||
|         joinRequests: [], // TODO
 |  | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,23 +1,46 @@ | ||||||
| import { Group } from '../entities/assignments/group.entity.js'; | import { Group } from '../entities/assignments/group.entity.js'; | ||||||
| import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js'; | import { mapToAssignment } from './assignment.js'; | ||||||
| import { mapToStudentDTO, StudentDTO } from './student.js'; | import { mapToStudent } from './student.js'; | ||||||
|  | import { mapToStudentDTO } from './student.js'; | ||||||
|  | import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group'; | ||||||
|  | import { getGroupRepository } from '../data/repositories.js'; | ||||||
|  | import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; | ||||||
|  | import { Class } from '../entities/classes/class.entity.js'; | ||||||
|  | import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||||
| 
 | 
 | ||||||
| export interface GroupDTO { | export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group { | ||||||
|     assignment: number | AssignmentDTO; |     const assignmentDto = groupDto.assignment as AssignmentDTO; | ||||||
|     groupNumber: number; | 
 | ||||||
|     members: string[] | StudentDTO[]; |     return getGroupRepository().create({ | ||||||
|  |         groupNumber: groupDto.groupNumber, | ||||||
|  |         assignment: mapToAssignment(assignmentDto, clazz), | ||||||
|  |         members: groupDto.members!.map((studentDto) => mapToStudent(studentDto as StudentDTO)), | ||||||
|  |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToGroupDTO(group: Group): GroupDTO { | export function mapToGroupDTO(group: Group, cls: Class): GroupDTO { | ||||||
|     return { |     return { | ||||||
|         assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within),
 |         class: cls.classId!, | ||||||
|  |         assignment: group.assignment.id!, | ||||||
|         groupNumber: group.groupNumber!, |         groupNumber: group.groupNumber!, | ||||||
|         members: group.members.map(mapToStudentDTO), |         members: group.members.map(mapToStudentDTO), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToGroupDTOId(group: Group): GroupDTO { | export function mapToGroupDTOId(group: Group, cls: Class): GroupDTOId { | ||||||
|     return { |     return { | ||||||
|  |         class: cls.classId!, | ||||||
|  |         assignment: group.assignment.id!, | ||||||
|  |         groupNumber: group.groupNumber!, | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Map to group DTO where other objects are only referenced by their id. | ||||||
|  |  */ | ||||||
|  | export function mapToShallowGroupDTO(group: Group): GroupDTO { | ||||||
|  |     return { | ||||||
|  |         class: group.assignment.within.classId!, | ||||||
|         assignment: group.assignment.id!, |         assignment: group.assignment.id!, | ||||||
|         groupNumber: group.groupNumber!, |         groupNumber: group.groupNumber!, | ||||||
|         members: group.members.map((member) => member.username), |         members: group.members.map((member) => member.username), | ||||||
|  |  | ||||||
|  | @ -1,42 +1,47 @@ | ||||||
| import { Question } from '../entities/questions/question.entity.js'; | import { Question } from '../entities/questions/question.entity.js'; | ||||||
|  | import { mapToStudentDTO } from './student.js'; | ||||||
|  | import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
|  | import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
| import { mapToStudentDTO, StudentDTO } from './student.js'; | import { mapToGroupDTOId } from './group.js'; | ||||||
| 
 | 
 | ||||||
| export interface QuestionDTO { | function getLearningObjectIdentifier(question: Question): LearningObjectIdentifierDTO { | ||||||
|     learningObjectIdentifier: LearningObjectIdentifier; |     return { | ||||||
|     sequenceNumber?: number; |         hruid: question.learningObjectHruid, | ||||||
|     author: StudentDTO; |         language: question.learningObjectLanguage, | ||||||
|     timestamp?: string; |         version: question.learningObjectVersion, | ||||||
|     content: string; |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function mapToLearningObjectID(loID: LearningObjectIdentifierDTO): LearningObjectIdentifier { | ||||||
|  |     return { | ||||||
|  |         hruid: loID.hruid, | ||||||
|  |         language: loID.language, | ||||||
|  |         version: loID.version ?? 1, | ||||||
|  |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * 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, | ||||||
|         sequenceNumber: question.sequenceNumber!, |         sequenceNumber: question.sequenceNumber!, | ||||||
|         author: mapToStudentDTO(question.author), |         author: mapToStudentDTO(question.author), | ||||||
|  |         inGroup: mapToGroupDTOId(question.inGroup, question.inGroup.assignment?.within), | ||||||
|         timestamp: question.timestamp.toISOString(), |         timestamp: question.timestamp.toISOString(), | ||||||
|         content: question.content, |         content: question.content, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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!, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								backend/src/interfaces/student-request.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								backend/src/interfaces/student-request.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | import { mapToStudentDTO } from './student.js'; | ||||||
|  | import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js'; | ||||||
|  | import { getClassJoinRequestRepository } from '../data/repositories.js'; | ||||||
|  | import { Student } from '../entities/users/student.entity.js'; | ||||||
|  | import { Class } from '../entities/classes/class.entity.js'; | ||||||
|  | import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | ||||||
|  | import { ClassStatus } 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: ClassStatus.Open, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | @ -1,17 +1,6 @@ | ||||||
| import { Student } from '../entities/users/student.entity.js'; | import { Student } from '../entities/users/student.entity.js'; | ||||||
| 
 | import { getStudentRepository } from '../data/repositories.js'; | ||||||
| export interface StudentDTO { | import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||||
|     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 { | ||||||
|  | @ -23,7 +12,9 @@ export function mapToStudentDTO(student: Student): StudentDTO { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToStudent(studentData: StudentDTO): Student { | export function mapToStudent(studentData: StudentDTO): Student { | ||||||
|     const student = new Student(studentData.username, studentData.firstName, studentData.lastName); |     return getStudentRepository().create({ | ||||||
| 
 |         username: studentData.username, | ||||||
|     return student; |         firstName: studentData.firstName, | ||||||
|  |         lastName: studentData.lastName, | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,26 +1,10 @@ | ||||||
| import { Submission } from '../entities/assignments/submission.entity.js'; | import { Submission } from '../entities/assignments/submission.entity.js'; | ||||||
| import { Language } from '../entities/content/language.js'; | import { mapToGroupDTOId } from './group.js'; | ||||||
| import { GroupDTO, mapToGroupDTO } from './group.js'; | import { 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'; | import { getSubmissionRepository } from '../data/repositories.js'; | ||||||
| 
 | import { Student } from '../entities/users/student.entity.js'; | ||||||
| export interface SubmissionDTO { | import { Group } from '../entities/assignments/group.entity.js'; | ||||||
|     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 { | ||||||
|  | @ -29,11 +13,10 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { | ||||||
|             language: submission.learningObjectLanguage, |             language: submission.learningObjectLanguage, | ||||||
|             version: submission.learningObjectVersion, |             version: submission.learningObjectVersion, | ||||||
|         }, |         }, | ||||||
| 
 |  | ||||||
|         submissionNumber: submission.submissionNumber, |         submissionNumber: submission.submissionNumber, | ||||||
|         submitter: mapToStudentDTO(submission.submitter), |         submitter: mapToStudentDTO(submission.submitter), | ||||||
|         time: submission.submissionTime, |         time: submission.submissionTime, | ||||||
|         group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined, |         group: submission.onBehalfOf ? mapToGroupDTOId(submission.onBehalfOf, submission.onBehalfOf.assignment.within) : undefined, | ||||||
|         content: submission.content, |         content: submission.content, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | @ -48,17 +31,14 @@ export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { | export function mapToSubmission(submissionDTO: SubmissionDTO, submitter: Student, onBehalfOf: Group): Submission { | ||||||
|     const submission = new Submission(); |     return getSubmissionRepository().create({ | ||||||
|     submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid; |         learningObjectHruid: submissionDTO.learningObjectIdentifier.hruid, | ||||||
|     submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language; |         learningObjectLanguage: submissionDTO.learningObjectIdentifier.language, | ||||||
|     submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!; |         learningObjectVersion: submissionDTO.learningObjectIdentifier.version || 1, | ||||||
|     // Submission.submissionNumber = submissionDTO.submissionNumber;
 |         submitter: submitter, | ||||||
|     submission.submitter = mapToStudent(submissionDTO.submitter); |         submissionTime: new Date(), | ||||||
|     // Submission.submissionTime = submissionDTO.time;
 |         content: submissionDTO.content, | ||||||
|     // Submission.onBehalfOf =  submissionDTO.group!;
 |         onBehalfOf: onBehalfOf, | ||||||
|     // TODO fix group
 |     }); | ||||||
|     submission.content = submissionDTO.content; |  | ||||||
| 
 |  | ||||||
|     return submission; |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,18 +1,17 @@ | ||||||
| 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 { mapToUserDTO } from './user.js'; | ||||||
| import { mapToUserDTO, UserDTO } from './user.js'; | import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; | ||||||
| 
 | import { getTeacherInvitationRepository } from '../data/repositories.js'; | ||||||
| export interface TeacherInvitationDTO { | import { Teacher } from '../entities/users/teacher.entity.js'; | ||||||
|     sender: string | UserDTO; | import { Class } from '../entities/classes/class.entity.js'; | ||||||
|     receiver: string | UserDTO; | import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | ||||||
|     class: string | ClassDTO; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { | export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { | ||||||
|     return { |     return { | ||||||
|         sender: mapToUserDTO(invitation.sender), |         sender: mapToUserDTO(invitation.sender), | ||||||
|         receiver: mapToUserDTO(invitation.receiver), |         receiver: mapToUserDTO(invitation.receiver), | ||||||
|         class: mapToClassDTO(invitation.class), |         classId: invitation.class.classId!, | ||||||
|  |         status: invitation.status, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -20,6 +19,16 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea | ||||||
|     return { |     return { | ||||||
|         sender: invitation.sender.username, |         sender: invitation.sender.username, | ||||||
|         receiver: invitation.receiver.username, |         receiver: invitation.receiver.username, | ||||||
|         class: invitation.class.classId!, |         classId: invitation.class.classId!, | ||||||
|  |         status: invitation.status, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function mapToInvitation(sender: Teacher, receiver: Teacher, cls: Class): TeacherInvitation { | ||||||
|  |     return getTeacherInvitationRepository().create({ | ||||||
|  |         sender, | ||||||
|  |         receiver, | ||||||
|  |         class: cls, | ||||||
|  |         status: ClassStatus.Open, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,17 +1,6 @@ | ||||||
| import { Teacher } from '../entities/users/teacher.entity.js'; | import { Teacher } from '../entities/users/teacher.entity.js'; | ||||||
| 
 | import { getTeacherRepository } from '../data/repositories.js'; | ||||||
| export interface TeacherDTO { | import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||||
|     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 { | ||||||
|  | @ -22,8 +11,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function mapToTeacher(TeacherData: TeacherDTO): Teacher { | export function mapToTeacher(teacherData: TeacherDTO): Teacher { | ||||||
|     const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName); |     return getTeacherRepository().create({ | ||||||
| 
 |         username: teacherData.username, | ||||||
|     return teacher; |         firstName: teacherData.firstName, | ||||||
|  |         lastName: teacherData.lastName, | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,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 { | ||||||
|  |  | ||||||
|  | @ -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,30 @@ 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.simple()), | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (getEnvVar(EnvVars.RunMode) === 'dev') { |     if (getEnvVar(envVars.RunMode) === 'dev') { | ||||||
|         return createLogger({ |         logger = createLogger({ | ||||||
|             transports: [consoleTransport], |             transports: [consoleTransport], | ||||||
|         }); |         }); | ||||||
|  |         logger.debug(`Logger initialized with level ${logLevel} to console`); | ||||||
|  |         return logger; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const lokiHost = getEnvVar(EnvVars.LokiHost); |     const lokiHost = getEnvVar(envVars.LokiHost); | ||||||
| 
 | 
 | ||||||
|     const lokiTransport: LokiTransport = new LokiTransport({ |     const lokiTransport: LokiTransport = new LokiTransport({ | ||||||
|         host: lokiHost, |         host: lokiHost, | ||||||
|         labels: Labels, |         labels: lokiLabels, | ||||||
|         level: logLevel, |         level: logLevel, | ||||||
|         json: true, |         json: true, | ||||||
|         format: format.combine(format.timestamp(), format.json()), |         format: format.combine(format.timestamp(), format.json()), | ||||||
|         onConnectionError: (err) => { |         onConnectionError: (err): void => { | ||||||
|             // eslint-disable-next-line no-console
 |             // eslint-disable-next-line no-console
 | ||||||
|             console.error(`Connection error: ${err}`); |             console.error(`Connection error: ${err}`); | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  | @ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts'; | ||||||
| export class MikroOrmLogger extends DefaultLogger { | export class MikroOrmLogger extends DefaultLogger { | ||||||
|     private logger: Logger = getLogger(); |     private logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|     log(namespace: LoggerNamespace, message: string, context?: LogContext) { |     static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown { | ||||||
|  |         const labels: LokiLabels = { | ||||||
|  |             service: 'ORM', | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let message: string; | ||||||
|  |         if (context !== undefined && context.labels !== undefined) { | ||||||
|  |             message = `[${namespace}] (${context.label}) ${messageArg}`; | ||||||
|  |         } else { | ||||||
|  |             message = `[${namespace}] ${messageArg}`; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             message: message, | ||||||
|  |             labels: labels, | ||||||
|  |             context: context, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     log(namespace: LoggerNamespace, message: string, context?: LogContext): void { | ||||||
|         if (!this.isEnabled(namespace, context)) { |         if (!this.isEnabled(namespace, context)) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         switch (namespace) { |         switch (namespace) { | ||||||
|             case 'query': |             case 'query': | ||||||
|                 this.logger.debug(this.createMessage(namespace, message, context)); |                 this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'query-params': |             case 'query-params': | ||||||
|                 // TODO Which log level should this be?
 |                 // TODO Which log level should this be?
 | ||||||
|                 this.logger.info(this.createMessage(namespace, message, context)); |                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'schema': |             case 'schema': | ||||||
|                 this.logger.info(this.createMessage(namespace, message, context)); |                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'discovery': |             case 'discovery': | ||||||
|                 this.logger.debug(this.createMessage(namespace, message, context)); |                 this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'info': |             case 'info': | ||||||
|                 this.logger.info(this.createMessage(namespace, message, context)); |                 this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             case 'deprecated': |             case 'deprecated': | ||||||
|                 this.logger.warn(this.createMessage(namespace, message, context)); |                 this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                 break; |                 break; | ||||||
|             default: |             default: | ||||||
|                 switch (context?.level) { |                 switch (context?.level) { | ||||||
|                     case 'info': |                     case 'info': | ||||||
|                         this.logger.info(this.createMessage(namespace, message, context)); |                         this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); | ||||||
|                         break; |                         break; | ||||||
|                     case 'warning': |                     case 'warning': | ||||||
|                         this.logger.warn(message); |                         this.logger.warn(message); | ||||||
|  | @ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger { | ||||||
|                 } |                 } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) { |  | ||||||
|         const labels: LokiLabels = { |  | ||||||
|             service: 'ORM', |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         let message: string; |  | ||||||
|         if (context?.label) { |  | ||||||
|             message = `[${namespace}] (${context?.label}) ${messageArg}`; |  | ||||||
|         } else { |  | ||||||
|             message = `[${namespace}] ${messageArg}`; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return { |  | ||||||
|             message: message, |  | ||||||
|             labels: labels, |  | ||||||
|             context: context, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { getLogger, Logger } from './initalize.js'; | import { getLogger, Logger } from './initalize.js'; | ||||||
| import { Request, Response } from 'express'; | import { Request, Response } from 'express'; | ||||||
| 
 | 
 | ||||||
| export function responseTimeLogger(req: Request, res: Response, time: number) { | export function responseTimeLogger(req: Request, res: Response, time: number): void { | ||||||
|     const logger: Logger = getLogger(); |     const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|     const method = req.method; |     const method = req.method; | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| import { EnvVars, getEnvVar } from '../../util/envvars.js'; | import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||||
| import { expressjwt } from 'express-jwt'; | import { expressjwt } from 'express-jwt'; | ||||||
|  | import * as jwt from 'jsonwebtoken'; | ||||||
| import { JwtPayload } from 'jsonwebtoken'; | import { JwtPayload } from 'jsonwebtoken'; | ||||||
| import jwksClient from 'jwks-rsa'; | import jwksClient from 'jwks-rsa'; | ||||||
| import * as express from 'express'; | import * as express from 'express'; | ||||||
| import * as jwt from 'jsonwebtoken'; |  | ||||||
| import { AuthenticatedRequest } from './authenticated-request.js'; | import { AuthenticatedRequest } from './authenticated-request.js'; | ||||||
| import { AuthenticationInfo } from './authentication-info.js'; | import { AuthenticationInfo } from './authentication-info.js'; | ||||||
| import { ForbiddenException, UnauthorizedException } from '../../exceptions.js'; | import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; | ||||||
|  | import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; | ||||||
| 
 | 
 | ||||||
| const JWKS_CACHE = true; | const JWKS_CACHE = true; | ||||||
| const JWKS_RATE_LIMIT = true; | const JWKS_RATE_LIMIT = true; | ||||||
|  | @ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient { | ||||||
| 
 | 
 | ||||||
| const idpConfigs = { | const idpConfigs = { | ||||||
|     student: { |     student: { | ||||||
|         issuer: getEnvVar(EnvVars.IdpStudentUrl), |         issuer: getEnvVar(envVars.IdpStudentUrl), | ||||||
|         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), |         jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)), | ||||||
|     }, |     }, | ||||||
|     teacher: { |     teacher: { | ||||||
|         issuer: getEnvVar(EnvVars.IdpTeacherUrl), |         issuer: getEnvVar(envVars.IdpTeacherUrl), | ||||||
|         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), |         jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)), | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -47,14 +48,14 @@ const idpConfigs = { | ||||||
| const verifyJwtToken = expressjwt({ | const verifyJwtToken = expressjwt({ | ||||||
|     secret: async (_: express.Request, token: jwt.Jwt | undefined) => { |     secret: async (_: express.Request, token: jwt.Jwt | undefined) => { | ||||||
|         if (!token?.payload || !(token.payload as JwtPayload).iss) { |         if (!token?.payload || !(token.payload as JwtPayload).iss) { | ||||||
|             throw new Error('Invalid token'); |             throw new UnauthorizedException('Invalid token.'); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const issuer = (token.payload as JwtPayload).iss; |         const issuer = (token.payload as JwtPayload).iss; | ||||||
| 
 | 
 | ||||||
|         const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer); |         const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer); | ||||||
|         if (!idpConfig) { |         if (!idpConfig) { | ||||||
|             throw new Error('Issuer not accepted.'); |             throw new UnauthorizedException('Issuer not accepted.'); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid); |         const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid); | ||||||
|  | @ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({ | ||||||
|         } |         } | ||||||
|         return signingKey.getPublicKey(); |         return signingKey.getPublicKey(); | ||||||
|     }, |     }, | ||||||
|     audience: getEnvVar(EnvVars.IdpAudience), |     audience: getEnvVar(envVars.IdpAudience), | ||||||
|     algorithms: [JWT_ALGORITHM], |     algorithms: [JWT_ALGORITHM], | ||||||
|     credentialsRequired: false, |     credentialsRequired: false, | ||||||
|     requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, |     requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, | ||||||
|  | @ -74,7 +75,7 @@ const verifyJwtToken = expressjwt({ | ||||||
|  */ |  */ | ||||||
| function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { | function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { | ||||||
|     if (!req.jwtPayload) { |     if (!req.jwtPayload) { | ||||||
|         return; |         return undefined; | ||||||
|     } |     } | ||||||
|     const issuer = req.jwtPayload.iss; |     const issuer = req.jwtPayload.iss; | ||||||
|     let accountType: 'student' | 'teacher'; |     let accountType: 'student' | 'teacher'; | ||||||
|  | @ -84,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | | ||||||
|     } else if (issuer === idpConfigs.teacher.issuer) { |     } else if (issuer === idpConfigs.teacher.issuer) { | ||||||
|         accountType = 'teacher'; |         accountType = 'teacher'; | ||||||
|     } else { |     } else { | ||||||
|         return; |         return undefined; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|         accountType: accountType, |         accountType: accountType, | ||||||
|         username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, |         username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, | ||||||
|  | @ -100,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | | ||||||
|  * Add the AuthenticationInfo object with the information about the current authentication to the request in order |  * Add the AuthenticationInfo object with the information about the current authentication to the request in order | ||||||
|  * to avoid that the routers have to deal with the JWT token. |  * to avoid that the routers have to deal with the JWT token. | ||||||
|  */ |  */ | ||||||
| const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => { | function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void { | ||||||
|     req.auth = getAuthenticationInfo(req); |     req.auth = getAuthenticationInfo(req); | ||||||
|     next(); |     next(); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | ||||||
| 
 | 
 | ||||||
|  | @ -113,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | ||||||
|  * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates |  * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates | ||||||
|  *                        to true. |  *                        to true. | ||||||
|  */ |  */ | ||||||
| export const authorize = | export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) { | ||||||
|     (accessCondition: (auth: AuthenticationInfo) => boolean) => |     return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => { | ||||||
|     (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => { |  | ||||||
|         if (!req.auth) { |         if (!req.auth) { | ||||||
|             throw new UnauthorizedException(); |             throw new UnauthorizedException(); | ||||||
|         } else if (!accessCondition(req.auth)) { |         } else if (!accessCondition(req.auth)) { | ||||||
|  | @ -124,6 +125,7 @@ export const authorize = | ||||||
|             next(); |             next(); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Middleware which rejects all unauthenticated users, but accepts all authenticated users. |  * Middleware which rejects all unauthenticated users, but accepts all authenticated users. | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| /** | /** | ||||||
|  * Object with information about the user who is currently logged in. |  * Object with information about the user who is currently logged in. | ||||||
|  */ |  */ | ||||||
| export type AuthenticationInfo = { | export interface AuthenticationInfo { | ||||||
|     accountType: 'student' | 'teacher'; |     accountType: 'student' | 'teacher'; | ||||||
|     username: string; |     username: string; | ||||||
|     name?: string; |     name?: string; | ||||||
|     firstName?: string; |     firstName?: string; | ||||||
|     lastName?: string; |     lastName?: string; | ||||||
|     email?: string; |     email?: string; | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import cors from 'cors'; | import cors from 'cors'; | ||||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | import { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
| 
 | 
 | ||||||
| export default cors({ | export default cors({ | ||||||
|     origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','), |     origin: getEnvVar(envVars.CorsAllowedOrigins).split(','), | ||||||
|     allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','), |     allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','), | ||||||
| }); | }); | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | import { NextFunction, Request, Response } from 'express'; | ||||||
|  | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
|  | import { hasStatusCode } from '../../exceptions/has-status-code.js'; | ||||||
|  | 
 | ||||||
|  | const logger: Logger = getLogger(); | ||||||
|  | 
 | ||||||
|  | export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void { | ||||||
|  |     if (hasStatusCode(err)) { | ||||||
|  |         logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); | ||||||
|  |         res.status(err.status).json(err); | ||||||
|  |     } else { | ||||||
|  |         logger.error(`Unexpected error occurred while handing a request: ${(err as { stack: string })?.stack ?? JSON.stringify(err)}`); | ||||||
|  |         res.status(500).json(err); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { LoggerOptions, Options } from '@mikro-orm/core'; | import { LoggerOptions, Options } from '@mikro-orm/core'; | ||||||
| import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | ||||||
| import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; | import { envVars, getEnvVar, getNumericEnvVar } from './util/envVars.js'; | ||||||
| import { SqliteDriver } from '@mikro-orm/sqlite'; | import { SqliteDriver } from '@mikro-orm/sqlite'; | ||||||
| import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | ||||||
| 
 | 
 | ||||||
|  | @ -42,33 +42,35 @@ const entities = [ | ||||||
|     Question, |     Question, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| function config(testingMode: boolean = false): Options { | function config(testingMode = false): Options { | ||||||
|     if (testingMode) { |     if (testingMode) { | ||||||
|         return { |         return { | ||||||
|             driver: SqliteDriver, |             driver: SqliteDriver, | ||||||
|             dbName: getEnvVar(EnvVars.DbName), |             dbName: getEnvVar(envVars.DbName), | ||||||
|             subscribers: [new SqliteAutoincrementSubscriber()], |             subscribers: [new SqliteAutoincrementSubscriber()], | ||||||
|             entities: entities, |             entities: entities, | ||||||
|  |             persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
 | ||||||
|             // EntitiesTs: entitiesTs,
 |             // EntitiesTs: entitiesTs,
 | ||||||
| 
 | 
 | ||||||
|             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 |             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 | ||||||
|             // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
 |             // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
 | ||||||
|             dynamicImportProvider: (id) => import(id), |             dynamicImportProvider: async (id) => import(id), | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         driver: PostgreSqlDriver, |         driver: PostgreSqlDriver, | ||||||
|         host: getEnvVar(EnvVars.DbHost), |         host: getEnvVar(envVars.DbHost), | ||||||
|         port: getNumericEnvVar(EnvVars.DbPort), |         port: getNumericEnvVar(envVars.DbPort), | ||||||
|         dbName: getEnvVar(EnvVars.DbName), |         dbName: getEnvVar(envVars.DbName), | ||||||
|         user: getEnvVar(EnvVars.DbUsername), |         user: getEnvVar(envVars.DbUsername), | ||||||
|         password: getEnvVar(EnvVars.DbPassword), |         password: getEnvVar(envVars.DbPassword), | ||||||
|         entities: entities, |         entities: entities, | ||||||
|  |         persistOnCreate: false, // Do not implicitly save entities when they are created via `create`.
 | ||||||
|         // EntitiesTs: entitiesTs,
 |         // EntitiesTs: entitiesTs,
 | ||||||
| 
 | 
 | ||||||
|         // Logging
 |         // Logging
 | ||||||
|         debug: getEnvVar(EnvVars.LogLevel) === 'debug', |         debug: getEnvVar(envVars.LogLevel) === 'debug', | ||||||
|         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), |         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { EntityManager, MikroORM } from '@mikro-orm/core'; | import { EntityManager, IDatabaseDriver, 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<MikroORM<IDatabaseDriver, EntityManager>> { | ||||||
|     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(); | ||||||
|  | @ -25,6 +25,8 @@ export async function initORM(testingMode: boolean = false) { | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     return orm; | ||||||
| } | } | ||||||
| export function forkEntityManager(): EntityManager { | export function forkEntityManager(): EntityManager { | ||||||
|     if (!orm) { |     if (!orm) { | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								backend/src/routes/answers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								backend/src/routes/answers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | import express from 'express'; | ||||||
|  | import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; | ||||||
|  | 
 | ||||||
|  | const router = express.Router({ mergeParams: true }); | ||||||
|  | 
 | ||||||
|  | router.get('/', getAllAnswersHandler); | ||||||
|  | 
 | ||||||
|  | router.post('/', createAnswerHandler); | ||||||
|  | 
 | ||||||
|  | router.get('/:seqAnswer', getAnswerHandler); | ||||||
|  | 
 | ||||||
|  | router.delete('/:seqAnswer', deleteAnswerHandler); | ||||||
|  | 
 | ||||||
|  | router.put('/:seqAnswer', updateAnswerHandler); | ||||||
|  | 
 | ||||||
|  | export default router; | ||||||
|  | @ -1,29 +1,30 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { | import { | ||||||
|     createAssignmentHandler, |     createAssignmentHandler, | ||||||
|  |     deleteAssignmentHandler, | ||||||
|     getAllAssignmentsHandler, |     getAllAssignmentsHandler, | ||||||
|     getAssignmentHandler, |     getAssignmentHandler, | ||||||
|  |     getAssignmentQuestionsHandler, | ||||||
|     getAssignmentsSubmissionsHandler, |     getAssignmentsSubmissionsHandler, | ||||||
|  |     putAssignmentHandler, | ||||||
| } from '../controllers/assignments.js'; | } from '../controllers/assignments.js'; | ||||||
| import groupRouter from './groups.js'; | import groupRouter from './groups.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 |  | ||||||
| router.get('/', getAllAssignmentsHandler); | router.get('/', getAllAssignmentsHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createAssignmentHandler); | router.post('/', createAssignmentHandler); | ||||||
| 
 | 
 | ||||||
| // Information about an assignment with id 'id'
 |  | ||||||
| router.get('/:id', getAssignmentHandler); | router.get('/:id', getAssignmentHandler); | ||||||
| 
 | 
 | ||||||
|  | router.put('/:id', putAssignmentHandler); | ||||||
|  | 
 | ||||||
|  | router.delete('/:id', deleteAssignmentHandler); | ||||||
|  | 
 | ||||||
| router.get('/:id/submissions', getAssignmentsSubmissionsHandler); | router.get('/:id/submissions', getAssignmentsSubmissionsHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/questions', (req, res) => { | router.get('/:id/questions', getAssignmentQuestionsHandler); | ||||||
|     res.json({ |  | ||||||
|         questions: ['0'], |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| router.use('/:assignmentid/groups', groupRouter); | router.use('/:assignmentid/groups', groupRouter); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,26 +1,28 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { getFrontendAuthConfig } from '../controllers/auth.js'; | import { getFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js'; | ||||||
| import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js'; | import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js'; | ||||||
| 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!' }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | router.post('/hello', authenticatedOnly, postHelloHandler); | ||||||
|  | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -1,10 +1,17 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { | import { | ||||||
|  |     addClassStudentHandler, | ||||||
|  |     addClassTeacherHandler, | ||||||
|     createClassHandler, |     createClassHandler, | ||||||
|  |     deleteClassHandler, | ||||||
|  |     deleteClassStudentHandler, | ||||||
|  |     deleteClassTeacherHandler, | ||||||
|     getAllClassesHandler, |     getAllClassesHandler, | ||||||
|     getClassHandler, |     getClassHandler, | ||||||
|     getClassStudentsHandler, |     getClassStudentsHandler, | ||||||
|  |     getClassTeachersHandler, | ||||||
|     getTeacherInvitationsHandler, |     getTeacherInvitationsHandler, | ||||||
|  |     putClassHandler, | ||||||
| } from '../controllers/classes.js'; | } from '../controllers/classes.js'; | ||||||
| import assignmentRouter from './assignments.js'; | import assignmentRouter from './assignments.js'; | ||||||
| 
 | 
 | ||||||
|  | @ -15,13 +22,26 @@ router.get('/', getAllClassesHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createClassHandler); | router.post('/', createClassHandler); | ||||||
| 
 | 
 | ||||||
| // Information about an class with id 'id'
 |  | ||||||
| router.get('/:id', getClassHandler); | router.get('/:id', getClassHandler); | ||||||
| 
 | 
 | ||||||
|  | router.put('/:id', putClassHandler); | ||||||
|  | 
 | ||||||
|  | router.delete('/:id', deleteClassHandler); | ||||||
|  | 
 | ||||||
| router.get('/:id/teacher-invitations', getTeacherInvitationsHandler); | router.get('/:id/teacher-invitations', getTeacherInvitationsHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/students', getClassStudentsHandler); | router.get('/:id/students', getClassStudentsHandler); | ||||||
| 
 | 
 | ||||||
|  | router.post('/:id/students', addClassStudentHandler); | ||||||
|  | 
 | ||||||
|  | router.delete('/:id/students/:username', deleteClassStudentHandler); | ||||||
|  | 
 | ||||||
|  | router.get('/:id/teachers', getClassTeachersHandler); | ||||||
|  | 
 | ||||||
|  | router.post('/:id/teachers', addClassTeacherHandler); | ||||||
|  | 
 | ||||||
|  | router.delete('/:id/teachers/:username', deleteClassTeacherHandler); | ||||||
|  | 
 | ||||||
| router.use('/:classid/assignments', assignmentRouter); | router.use('/:classid/assignments', assignmentRouter); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,13 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js'; | import { | ||||||
|  |     createGroupHandler, | ||||||
|  |     deleteGroupHandler, | ||||||
|  |     getAllGroupsHandler, | ||||||
|  |     getGroupHandler, | ||||||
|  |     getGroupQuestionsHandler, | ||||||
|  |     getGroupSubmissionsHandler, | ||||||
|  |     putGroupHandler, | ||||||
|  | } from '../controllers/groups.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
|  | @ -8,16 +16,14 @@ router.get('/', getAllGroupsHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createGroupHandler); | router.post('/', createGroupHandler); | ||||||
| 
 | 
 | ||||||
| // Information about a group (members, ... [TODO DOC])
 |  | ||||||
| router.get('/:groupid', getGroupHandler); | router.get('/:groupid', getGroupHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:groupid', getGroupSubmissionsHandler); | router.put('/:groupid', putGroupHandler); | ||||||
| 
 | 
 | ||||||
| // The list of questions a group has made
 | router.delete('/:groupid', deleteGroupHandler); | ||||||
| router.get('/:id/questions', (req, res) => { | 
 | ||||||
|     res.json({ | router.get('/:groupid/submissions', getGroupSubmissionsHandler); | ||||||
|         questions: ['0'], | 
 | ||||||
|     }); | router.get('/:groupid/questions', getGroupQuestionsHandler); | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -1,11 +1,7 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { | import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; | ||||||
|     createQuestionHandler, | import answerRoutes from './answers.js'; | ||||||
|     deleteQuestionHandler, | 
 | ||||||
|     getAllQuestionsHandler, |  | ||||||
|     getQuestionAnswersHandler, |  | ||||||
|     getQuestionHandler, |  | ||||||
| } from '../controllers/questions.js'; |  | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| // Query language
 | // Query language
 | ||||||
|  | @ -20,6 +16,6 @@ router.delete('/:seq', deleteQuestionHandler); | ||||||
| // Information about a question with id
 | // Information about a question with id
 | ||||||
| router.get('/:seq', getQuestionHandler); | router.get('/:seq', getQuestionHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/answers/:seq', getQuestionAnswersHandler); | router.use('/:seq/answers', answerRoutes); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl