Merge remote-tracking branch 'origin/dev' into chore/docker

This commit is contained in:
Timo De Meyst 2025-03-09 23:49:10 +01:00
commit f6859b6748
51 changed files with 212 additions and 591 deletions

View file

@ -10,7 +10,7 @@ Projectopgave</a></span>
</p> </p>
<ul align="center" style="list-style-type: none"> <ul align="center" style="list-style-type: none">
<li>Projectleider: Fransisco Van Langenhove (<a href="https://github.com/Gabriellvl">@Gabriellvl</a>)</li> <li>Projectleider: Fransisco Gabriel Van Langenhove (<a href="https://github.com/Gabriellvl">@Gabriellvl</a>)</li>
<li>Technische lead: Tibo De Peuter (<a href="https://github.com/tdpeuter">@tdpeuter</a>)</li> <li>Technische lead: Tibo De Peuter (<a href="https://github.com/tdpeuter">@tdpeuter</a>)</li>
<li>Systeembeheerder: Timo De Meyst (<a href="https://github.com/kloep1">@kloep1</a>)</li> <li>Systeembeheerder: Timo De Meyst (<a href="https://github.com/kloep1">@kloep1</a>)</li>
<li>Customer relations officer: Adriaan Jacquet (<a href="https://github.com/WhisperinCheetah">@WhisperinCheetah</a>)</li> <li>Customer relations officer: Adriaan Jacquet (<a href="https://github.com/WhisperinCheetah">@WhisperinCheetah</a>)</li>
@ -21,17 +21,28 @@ en lessen kunnen samenstellen hun leerlingen en hun vooruitgang kunnen opvolgen.
## Installatie ## Installatie
Om de applicatie in te stellen voor een productieomgeving, volg de [installatiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving).
Alternatief kan je één van de volgende methodes gebruiken om de applicatie lokaal te draaien.
### Quick start ### Quick start
1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/) en [Docker Compose](https://docs.docker.com/compose/)). 1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/) en [Docker Compose](https://docs.docker.com/compose/)).
2. Clone deze repository. 2. Clone deze repository.
3. Voer `docker compose up` uit in de root van de repository. 3. In de backend, kopieer `.env.example` (of `.env.development.example`) naar `.env` en pas de variabelen aan waar nodig.
4. Voer `docker compose up` uit in de root van de repository.
5. Optioneel: Configureer de applicatie aan de hand van de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie).
```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 cd Dwengo-1/backend
cp .env.example .env
# Pas .env aan
nano .env
cd ..
docker compose up docker compose up
# Configureer de applicatie
``` ```
### Handmatige installatie ### Handmatige installatie
@ -46,8 +57,9 @@ De tech-stack bestaat uit:
- **Frontend**: TypeScript + Vue.js + Vuetify - **Frontend**: TypeScript + Vue.js + Vuetify
- **Backend**: TypeScript + Node.js + Express.js + TypeORM + PostgreSQL - **Backend**: TypeScript + Node.js + Express.js + TypeORM + PostgreSQL
- **Identity provider**: Keycloak
Voor meer informatie over de keuze van deze tech-stack, zie [designkeuzes](https://github.com/SELab-2/Dwengo-1/wiki/Design-keuzes). Voor meer informatie over de keuze van deze tech-stack, zie [designkeuzes](https://github.com/SELab-2/Dwengo-1/wiki/Developer:-Design-keuzes).
## Bijdragen aan Dwengo-1 ## Bijdragen aan Dwengo-1

BIN
assets/img/keycloak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -20,3 +20,10 @@ npm run dev
npm run build npm run build
npm run start npm run start
``` ```
## Keycloak configuratie
Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt.
Voor productie is het ten sterkste aangeraden om keycloak manueel te configureren.
Voor meer informatie, zie de [administrator-handleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#installatie-en-server-configuratie).

View file

@ -1,10 +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';

View file

@ -6,7 +6,5 @@ export const DWENGO_API_BASE: string = 'https://dwengo.org/backend/api';
// Logging // Logging
export const LOG_LEVEL: string = export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info';
'development' === process.env.NODE_ENV ? 'debug' : 'info'; export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102';
export const LOKI_HOST: string =
process.env.LOKI_HOST || 'http://localhost:3102';

View file

@ -1,17 +1,10 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { import { getLearningObjectById, getLearningObjectIdsFromPath, getLearningObjectsFromPath } from '../services/learningObjects.js';
getLearningObjectById,
getLearningObjectIdsFromPath,
getLearningObjectsFromPath,
} from '../services/learningObjects.js';
import { FALLBACK_LANG } from '../config.js'; import { FALLBACK_LANG } from '../config.js';
import { FilteredLearningObject } from '../interfaces/learningPath.js'; import { FilteredLearningObject } from '../interfaces/learningPath.js';
import { getLogger } from '../logging/initalize.js'; import { getLogger } from '../logging/initalize.js';
export async function getAllLearningObjects( export async function getAllLearningObjects(req: Request, res: Response): Promise<void> {
req: Request,
res: Response
): Promise<void> {
try { try {
const hruid = req.query.hruid as string; const hruid = req.query.hruid as string;
const full = req.query.full === 'true'; const full = req.query.full === 'true';
@ -26,10 +19,7 @@ export async function getAllLearningObjects(
if (full) { if (full) {
learningObjects = await getLearningObjectsFromPath(hruid, language); learningObjects = await getLearningObjectsFromPath(hruid, language);
} else { } else {
learningObjects = await getLearningObjectIdsFromPath( learningObjects = await getLearningObjectIdsFromPath(hruid, language);
hruid,
language
);
} }
res.json(learningObjects); res.json(learningObjects);
@ -39,10 +29,7 @@ export async function getAllLearningObjects(
} }
} }
export async function getLearningObject( export async function getLearningObject(req: Request, res: Response): Promise<void> {
req: Request,
res: Response
): Promise<void> {
try { try {
const { hruid } = req.params; const { hruid } = req.params;
const language = (req.query.language as string) || FALLBACK_LANG; const language = (req.query.language as string) || FALLBACK_LANG;

View file

@ -1,18 +1,12 @@
import { Request, Response } from 'express'; 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 { import { fetchLearningPaths, searchLearningPaths } from '../services/learningPaths.js';
fetchLearningPaths,
searchLearningPaths,
} from '../services/learningPaths.js';
import { getLogger } from '../logging/initalize.js'; import { getLogger } from '../logging/initalize.js';
/** /**
* Fetch learning paths based on query parameters. * Fetch learning paths based on query parameters.
*/ */
export async function getLearningPaths( export async function getLearningPaths(req: Request, res: Response): Promise<void> {
req: Request,
res: Response
): Promise<void> {
try { try {
const hruids = req.query.hruid; const hruids = req.query.hruid;
const themeKey = req.query.theme as string; const themeKey = req.query.theme as string;
@ -22,13 +16,9 @@ export async function getLearningPaths(
let hruidList; let hruidList;
if (hruids) { if (hruids) {
hruidList = Array.isArray(hruids) hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)];
? hruids.map(String)
: [String(hruids)];
} else if (themeKey) { } else if (themeKey) {
const theme = themes.find((t) => { const theme = themes.find((t) => t.title === themeKey);
return t.title === themeKey;
});
if (theme) { if (theme) {
hruidList = theme.hruids; hruidList = theme.hruids;
} else { } else {
@ -38,29 +28,17 @@ export async function getLearningPaths(
return; return;
} }
} else if (searchQuery) { } else if (searchQuery) {
const searchResults = await searchLearningPaths( const searchResults = await searchLearningPaths(searchQuery, language);
searchQuery,
language
);
res.json(searchResults); res.json(searchResults);
return; return;
} else { } else {
hruidList = themes.flatMap((theme) => { hruidList = themes.flatMap((theme) => theme.hruids);
return theme.hruids;
});
} }
const learningPaths = await fetchLearningPaths( const learningPaths = await fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`);
hruidList,
language,
`HRUIDs: ${hruidList.join(', ')}`
);
res.json(learningPaths.data); res.json(learningPaths.data);
} catch (error) { } catch (error) {
getLogger().error( getLogger().error('❌ Unexpected error fetching learning paths:', error);
'❌ Unexpected error fetching learning paths:',
error
);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
} }

View file

@ -11,24 +11,19 @@ interface Translations {
export function getThemes(req: Request, res: Response) { export function getThemes(req: Request, res: Response) {
const language = (req.query.language as string)?.toLowerCase() || 'nl'; const language = (req.query.language as string)?.toLowerCase() || 'nl';
const translations = loadTranslations<Translations>(language); const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => { const themeList = themes.map((theme) => ({
return { key: theme.title,
key: theme.title, title: translations.curricula_page[theme.title]?.title || theme.title,
title: description: translations.curricula_page[theme.title]?.description,
translations.curricula_page[theme.title]?.title || theme.title, image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
description: translations.curricula_page[theme.title]?.description, }));
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
};
});
res.json(themeList); res.json(themeList);
} }
export function getThemeByTitle(req: Request, res: Response) { export function getThemeByTitle(req: Request, res: Response) {
const themeKey = req.params.theme; const themeKey = req.params.theme;
const theme = themes.find((t) => { const theme = themes.find((t) => t.title === themeKey);
return t.title === themeKey;
});
if (theme) { if (theme) {
res.json(theme.hruids); res.json(theme.hruids);

View file

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

View file

@ -3,24 +3,16 @@ import { Group } from '../../entities/assignments/group.entity.js';
import { Assignment } from '../../entities/assignments/assignment.entity.js'; import { Assignment } from '../../entities/assignments/assignment.entity.js';
export class GroupRepository extends DwengoEntityRepository<Group> { export class GroupRepository extends DwengoEntityRepository<Group> {
public findByAssignmentAndGroupNumber( public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
assignment: Assignment,
groupNumber: number
): Promise<Group | null> {
return this.findOne({ return this.findOne({
assignment: assignment, assignment: assignment,
groupNumber: groupNumber, groupNumber: groupNumber,
}); });
} }
public findAllGroupsForAssignment( public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
assignment: Assignment
): Promise<Group[]> {
return this.findAll({ where: { assignment: assignment } }); return this.findAll({ where: { assignment: assignment } });
} }
public deleteByAssignmentAndGroupNumber( public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) {
assignment: Assignment,
groupNumber: number
) {
return this.deleteWhere({ return this.deleteWhere({
assignment: assignment, assignment: assignment,
groupNumber: groupNumber, groupNumber: groupNumber,

View file

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

View file

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

View file

@ -3,10 +3,7 @@ import { Attachment } from '../../entities/content/attachment.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
export class AttachmentRepository extends DwengoEntityRepository<Attachment> { export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
public findByLearningObjectAndNumber( public findByLearningObjectAndNumber(learningObject: LearningObject, sequenceNumber: number) {
learningObject: LearningObject,
sequenceNumber: number
) {
return this.findOne({ return this.findOne({
learningObject: learningObject, learningObject: learningObject,
sequenceNumber: sequenceNumber, sequenceNumber: sequenceNumber,

View file

@ -3,9 +3,7 @@ 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';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public findByIdentifier( public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
identifier: LearningObjectIdentifier
): Promise<LearningObject | null> {
return this.findOne({ return this.findOne({
hruid: identifier.hruid, hruid: identifier.hruid,
language: identifier.language, language: identifier.language,

View file

@ -3,10 +3,7 @@ import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from '../../entities/content/language.js'; import { Language } from '../../entities/content/language.js';
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public findByHruidAndLanguage( public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
hruid: string,
language: Language
): Promise<LearningPath | null> {
return this.findOne({ hruid: hruid, language: language }); return this.findOne({ hruid: hruid, language: language });
} }
// This repository is read-only for now since creating own learning object is an extension feature. // This repository is read-only for now since creating own learning object is an extension feature.

View file

@ -1,8 +1,6 @@
import { EntityRepository, FilterQuery } from '@mikro-orm/core'; import { EntityRepository, FilterQuery } from '@mikro-orm/core';
export abstract class DwengoEntityRepository< export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
T extends object,
> extends EntityRepository<T> {
public async save(entity: T) { public async save(entity: T) {
const em = this.getEntityManager(); const em = this.getEntityManager();
em.persist(entity); em.persist(entity);

View file

@ -4,11 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js';
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
export class AnswerRepository extends DwengoEntityRepository<Answer> { export class AnswerRepository extends DwengoEntityRepository<Answer> {
public createAnswer(answer: { public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
toQuestion: Question;
author: Teacher;
content: string;
}): Promise<Answer> {
const answerEntity = new Answer(); const answerEntity = new Answer();
answerEntity.toQuestion = answer.toQuestion; answerEntity.toQuestion = answer.toQuestion;
answerEntity.author = answer.author; answerEntity.author = answer.author;
@ -21,10 +17,7 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
orderBy: { sequenceNumber: 'ASC' }, orderBy: { sequenceNumber: 'ASC' },
}); });
} }
public removeAnswerByQuestionAndSequenceNumber( public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
question: Question,
sequenceNumber: number
): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
toQuestion: question, toQuestion: question,
sequenceNumber: sequenceNumber, sequenceNumber: sequenceNumber,

View file

@ -4,11 +4,7 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
export class QuestionRepository extends DwengoEntityRepository<Question> { export class QuestionRepository extends DwengoEntityRepository<Question> {
public createQuestion(question: { public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
loId: LearningObjectIdentifier;
author: Student;
content: string;
}): Promise<Question> {
const questionEntity = new Question(); const questionEntity = new Question();
questionEntity.learningObjectHruid = question.loId.hruid; questionEntity.learningObjectHruid = question.loId.hruid;
questionEntity.learningObjectLanguage = question.loId.language; questionEntity.learningObjectLanguage = question.loId.language;
@ -17,9 +13,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
questionEntity.content = question.content; questionEntity.content = question.content;
return this.insert(questionEntity); return this.insert(questionEntity);
} }
public findAllQuestionsAboutLearningObject( public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
loId: LearningObjectIdentifier
): Promise<Question[]> {
return this.findAll({ return this.findAll({
where: { where: {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
@ -31,10 +25,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
}, },
}); });
} }
public removeQuestionByLearningObjectAndSequenceNumber( public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
loId: LearningObjectIdentifier,
sequenceNumber: number
): Promise<void> {
return this.deleteWhere({ return this.deleteWhere({
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,

View file

@ -1,9 +1,4 @@
import { import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-orm/core';
AnyEntity,
EntityManager,
EntityName,
EntityRepository,
} from '@mikro-orm/core';
import { forkEntityManager } from '../orm.js'; import { forkEntityManager } from '../orm.js';
import { StudentRepository } from './users/student-repository.js'; import { StudentRepository } from './users/student-repository.js';
import { Student } from '../entities/users/student.entity.js'; import { Student } from '../entities/users/student.entity.js';
@ -43,9 +38,7 @@ export function transactional<T>(f: () => Promise<T>) {
entityManager?.transactional(f); entityManager?.transactional(f);
} }
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>( function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {
entity: EntityName<T>
): () => R {
let cachedRepo: R | undefined; let cachedRepo: R | undefined;
return (): R => { return (): R => {
if (!cachedRepo) { if (!cachedRepo) {
@ -60,60 +53,24 @@ function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(
/* Users */ /* Users */
export const getUserRepository = repositoryGetter<User, UserRepository>(User); export const getUserRepository = repositoryGetter<User, UserRepository>(User);
export const getStudentRepository = repositoryGetter< export const getStudentRepository = repositoryGetter<Student, StudentRepository>(Student);
Student, export const getTeacherRepository = repositoryGetter<Teacher, TeacherRepository>(Teacher);
StudentRepository
>(Student);
export const getTeacherRepository = repositoryGetter<
Teacher,
TeacherRepository
>(Teacher);
/* Classes */ /* Classes */
export const getClassRepository = repositoryGetter<Class, ClassRepository>( export const getClassRepository = repositoryGetter<Class, ClassRepository>(Class);
Class export const getClassJoinRequestRepository = repositoryGetter<ClassJoinRequest, ClassJoinRequestRepository>(ClassJoinRequest);
); export const getTeacherInvitationRepository = repositoryGetter<TeacherInvitation, TeacherInvitationRepository>(TeacherInvitationRepository);
export const getClassJoinRequestRepository = repositoryGetter<
ClassJoinRequest,
ClassJoinRequestRepository
>(ClassJoinRequest);
export const getTeacherInvitationRepository = repositoryGetter<
TeacherInvitation,
TeacherInvitationRepository
>(TeacherInvitationRepository);
/* Assignments */ /* Assignments */
export const getAssignmentRepository = repositoryGetter< export const getAssignmentRepository = repositoryGetter<Assignment, AssignmentRepository>(Assignment);
Assignment, export const getGroupRepository = repositoryGetter<Group, GroupRepository>(Group);
AssignmentRepository export const getSubmissionRepository = repositoryGetter<Submission, SubmissionRepository>(Submission);
>(Assignment);
export const getGroupRepository = repositoryGetter<Group, GroupRepository>(
Group
);
export const getSubmissionRepository = repositoryGetter<
Submission,
SubmissionRepository
>(Submission);
/* Questions and answers */ /* Questions and answers */
export const getQuestionRepository = repositoryGetter< export const getQuestionRepository = repositoryGetter<Question, QuestionRepository>(Question);
Question, export const getAnswerRepository = repositoryGetter<Answer, AnswerRepository>(Answer);
QuestionRepository
>(Question);
export const getAnswerRepository = repositoryGetter<Answer, AnswerRepository>(
Answer
);
/* Learning content */ /* Learning content */
export const getLearningObjectRepository = repositoryGetter< export const getLearningObjectRepository = repositoryGetter<LearningObject, LearningObjectRepository>(LearningObject);
LearningObject, export const getLearningPathRepository = repositoryGetter<LearningPath, LearningPathRepository>(LearningPath);
LearningObjectRepository export const getAttachmentRepository = repositoryGetter<Attachment, AttachmentRepository>(Assignment);
>(LearningObject);
export const getLearningPathRepository = repositoryGetter<
LearningPath,
LearningPathRepository
>(LearningPath);
export const getAttachmentRepository = repositoryGetter<
Attachment,
AttachmentRepository
>(Assignment);

View file

@ -23,13 +23,7 @@ export const themes: Theme[] = [
}, },
{ {
title: 'art', title: 'art',
hruids: [ hruids: ['pn_werking', 'un_artificiele_intelligentie', 'art1', 'art2', 'art3'],
'pn_werking',
'un_artificiele_intelligentie',
'art1',
'art2',
'art3',
],
}, },
{ {
title: 'socialrobot', title: 'socialrobot',
@ -37,12 +31,7 @@ export const themes: Theme[] = [
}, },
{ {
title: 'agriculture', title: 'agriculture',
hruids: [ hruids: ['pn_werking', 'un_artificiele_intelligentie', 'agri_landbouw', 'agri_lopendeband'],
'pn_werking',
'un_artificiele_intelligentie',
'agri_landbouw',
'agri_lopendeband',
],
}, },
{ {
title: 'wegostem', title: 'wegostem',
@ -83,16 +72,7 @@ export const themes: Theme[] = [
}, },
{ {
title: 'python_programming', title: 'python_programming',
hruids: [ hruids: ['pn_werking', 'pn_datatypes', 'pn_operatoren', 'pn_structuren', 'pn_functies', 'art2', 'stem_insectbooks', 'un_algoenprog'],
'pn_werking',
'pn_datatypes',
'pn_operatoren',
'pn_structuren',
'pn_functies',
'art2',
'stem_insectbooks',
'un_algoenprog',
],
}, },
{ {
title: 'stem', title: 'stem',
@ -110,15 +90,7 @@ export const themes: Theme[] = [
}, },
{ {
title: 'care', title: 'care',
hruids: [ hruids: ['pn_werking', 'un_artificiele_intelligentie', 'aiz1_zorg', 'aiz2_grafen', 'aiz3_unplugged', 'aiz4_eindtermen', 'aiz5_triage'],
'pn_werking',
'un_artificiele_intelligentie',
'aiz1_zorg',
'aiz2_grafen',
'aiz3_unplugged',
'aiz4_eindtermen',
'aiz5_triage',
],
}, },
{ {
title: 'chatbot', title: 'chatbot',

View file

@ -1,23 +1,11 @@
import { import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
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 '../content/language.js';
@Entity() @Entity()
export class Assignment { export class Assignment {
@ManyToOne({ @ManyToOne({ entity: () => Class, primary: true })
entity: () => {
return Class;
},
primary: true,
})
within!: Class; within!: Class;
@PrimaryKey({ type: 'number' }) @PrimaryKey({ type: 'number' })
@ -32,18 +20,9 @@ export class Assignment {
@Property({ type: 'string' }) @Property({ type: 'string' })
learningPathHruid!: string; learningPathHruid!: string;
@Enum({ @Enum({ items: () => Language })
items: () => {
return Language;
},
})
learningPathLanguage!: Language; learningPathLanguage!: Language;
@OneToMany({ @OneToMany({ entity: () => Group, mappedBy: 'assignment' })
entity: () => {
return Group;
},
mappedBy: 'assignment',
})
groups!: Group[]; groups!: Group[];
} }

View file

@ -5,9 +5,7 @@ import { Student } from '../users/student.entity.js';
@Entity() @Entity()
export class Group { export class Group {
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Assignment,
return Assignment;
},
primary: true, primary: true,
}) })
assignment!: Assignment; assignment!: Assignment;
@ -16,9 +14,7 @@ export class Group {
groupNumber!: number; groupNumber!: number;
@ManyToMany({ @ManyToMany({
entity: () => { entity: () => Student,
return Student;
},
}) })
members!: Student[]; members!: Student[];
} }

View file

@ -9,9 +9,7 @@ export class Submission {
learningObjectHruid!: string; learningObjectHruid!: string;
@Enum({ @Enum({
items: () => { items: () => Language,
return Language;
},
primary: true, primary: true,
}) })
learningObjectLanguage!: Language; learningObjectLanguage!: Language;
@ -23,9 +21,7 @@ export class Submission {
submissionNumber!: number; submissionNumber!: number;
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Student,
return Student;
},
}) })
submitter!: Student; submitter!: Student;
@ -33,9 +29,7 @@ export class Submission {
submissionTime!: Date; submissionTime!: Date;
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Group,
return Group;
},
nullable: true, nullable: true,
}) })
onBehalfOf?: Group; onBehalfOf?: Group;

View file

@ -5,24 +5,18 @@ import { Class } from './class.entity.js';
@Entity() @Entity()
export class ClassJoinRequest { export class ClassJoinRequest {
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Student,
return Student;
},
primary: true, primary: true,
}) })
requester!: Student; requester!: Student;
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Class,
return Class;
},
primary: true, primary: true,
}) })
class!: Class; class!: Class;
@Enum(() => { @Enum(() => ClassJoinRequestStatus)
return ClassJoinRequestStatus;
})
status!: ClassJoinRequestStatus; status!: ClassJoinRequestStatus;
} }

View file

@ -1,10 +1,4 @@
import { import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core';
Collection,
Entity,
ManyToMany,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { v4 } from 'uuid'; 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';
@ -17,13 +11,9 @@ export class Class {
@Property({ type: 'string' }) @Property({ type: 'string' })
displayName!: string; displayName!: string;
@ManyToMany(() => { @ManyToMany(() => Teacher)
return Teacher;
})
teachers!: Collection<Teacher>; teachers!: Collection<Teacher>;
@ManyToMany(() => { @ManyToMany(() => Student)
return Student;
})
students!: Collection<Student>; students!: Collection<Student>;
} }

View file

@ -8,25 +8,19 @@ import { Class } from './class.entity.js';
@Entity() @Entity()
export class TeacherInvitation { export class TeacherInvitation {
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Teacher,
return Teacher;
},
primary: true, primary: true,
}) })
sender!: Teacher; sender!: Teacher;
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Teacher,
return Teacher;
},
primary: true, primary: true,
}) })
receiver!: Teacher; receiver!: Teacher;
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Class,
return Class;
},
primary: true, primary: true,
}) })
class!: Class; class!: Class;

View file

@ -4,9 +4,7 @@ import { LearningObject } from './learning-object.entity.js';
@Entity() @Entity()
export class Attachment { export class Attachment {
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => LearningObject,
return LearningObject;
},
primary: true, primary: true,
}) })
learningObject!: LearningObject; learningObject!: LearningObject;

View file

@ -1,13 +1,4 @@
import { import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
Embeddable,
Embedded,
Entity,
Enum,
ManyToMany,
OneToMany,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { Language } from './language.js'; import { Language } from './language.js';
import { Attachment } from './attachment.entity.js'; import { Attachment } from './attachment.entity.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
@ -18,9 +9,7 @@ export class LearningObject {
hruid!: string; hruid!: string;
@Enum({ @Enum({
items: () => { items: () => Language,
return Language;
},
primary: true, primary: true,
}) })
language!: Language; language!: Language;
@ -29,9 +18,7 @@ export class LearningObject {
version: string = '1'; version: string = '1';
@ManyToMany({ @ManyToMany({
entity: () => { entity: () => Teacher,
return Teacher;
},
}) })
admins!: Teacher[]; admins!: Teacher[];
@ -57,9 +44,7 @@ export class LearningObject {
skosConcepts!: string[]; skosConcepts!: string[];
@Embedded({ @Embedded({
entity: () => { entity: () => EducationalGoal,
return EducationalGoal;
},
array: true, array: true,
}) })
educationalGoals: EducationalGoal[] = []; educationalGoals: EducationalGoal[] = [];
@ -77,9 +62,7 @@ export class LearningObject {
estimatedTime!: number; estimatedTime!: number;
@Embedded({ @Embedded({
entity: () => { entity: () => ReturnValue,
return ReturnValue;
},
}) })
returnValue!: ReturnValue; returnValue!: ReturnValue;
@ -90,9 +73,7 @@ export class LearningObject {
contentLocation?: string; contentLocation?: string;
@OneToMany({ @OneToMany({
entity: () => { entity: () => Attachment,
return Attachment;
},
mappedBy: 'learningObject', mappedBy: 'learningObject',
}) })
attachments: Attachment[] = []; attachments: Attachment[] = [];

View file

@ -1,13 +1,4 @@
import { import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToOne, PrimaryKey, Property } from '@mikro-orm/core';
Embeddable,
Embedded,
Entity,
Enum,
ManyToMany,
OneToOne,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { Language } from './language.js'; import { Language } from './language.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
@ -17,17 +8,13 @@ export class LearningPath {
hruid!: string; hruid!: string;
@Enum({ @Enum({
items: () => { items: () => Language,
return Language;
},
primary: true, primary: true,
}) })
language!: Language; language!: Language;
@ManyToMany({ @ManyToMany({
entity: () => { entity: () => Teacher,
return Teacher;
},
}) })
admins!: Teacher[]; admins!: Teacher[];
@ -41,9 +28,7 @@ export class LearningPath {
image!: string; image!: string;
@Embedded({ @Embedded({
entity: () => { entity: () => LearningPathNode,
return LearningPathNode;
},
array: true, array: true,
}) })
nodes: LearningPathNode[] = []; nodes: LearningPathNode[] = [];
@ -55,9 +40,7 @@ export class LearningPathNode {
learningObjectHruid!: string; learningObjectHruid!: string;
@Enum({ @Enum({
items: () => { items: () => Language,
return Language;
},
}) })
language!: Language; language!: Language;
@ -71,9 +54,7 @@ export class LearningPathNode {
startNode!: boolean; startNode!: boolean;
@Embedded({ @Embedded({
entity: () => { entity: () => LearningPathTransition,
return LearningPathTransition;
},
array: true, array: true,
}) })
transitions!: LearningPathTransition[]; transitions!: LearningPathTransition[];
@ -85,9 +66,7 @@ export class LearningPathTransition {
condition!: string; condition!: string;
@OneToOne({ @OneToOne({
entity: () => { entity: () => LearningPathNode,
return LearningPathNode;
},
}) })
next!: LearningPathNode; next!: LearningPathNode;
} }

View file

@ -5,17 +5,13 @@ import { Teacher } from '../users/teacher.entity.js';
@Entity() @Entity()
export class Answer { export class Answer {
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Teacher,
return Teacher;
},
primary: true, primary: true,
}) })
author!: Teacher; author!: Teacher;
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Question,
return Question;
},
primary: true, primary: true,
}) })
toQuestion!: Question; toQuestion!: Question;

View file

@ -8,9 +8,7 @@ export class Question {
learningObjectHruid!: string; learningObjectHruid!: string;
@Enum({ @Enum({
items: () => { items: () => Language,
return Language;
},
primary: true, primary: true,
}) })
learningObjectLanguage!: Language; learningObjectLanguage!: Language;
@ -22,9 +20,7 @@ export class Question {
sequenceNumber!: number; sequenceNumber!: number;
@ManyToOne({ @ManyToOne({
entity: () => { entity: () => Student,
return Student;
},
}) })
author!: Student; author!: Student;

View file

@ -5,19 +5,13 @@ import { Group } from '../assignments/group.entity.js';
import { StudentRepository } from '../../data/users/student-repository.js'; import { StudentRepository } from '../../data/users/student-repository.js';
@Entity({ @Entity({
repository: () => { repository: () => StudentRepository,
return StudentRepository;
},
}) })
export class Student extends User { export class Student extends User {
@ManyToMany(() => { @ManyToMany(() => Class)
return Class;
})
classes!: Collection<Class>; classes!: Collection<Class>;
@ManyToMany(() => { @ManyToMany(() => Group)
return Group;
})
groups!: Collection<Group>; groups!: Collection<Group>;
constructor( constructor(

View file

@ -4,8 +4,6 @@ import { Class } from '../classes/class.entity.js';
@Entity() @Entity()
export class Teacher extends User { export class Teacher extends User {
@ManyToMany(() => { @ManyToMany(() => Class)
return Class;
})
classes!: Collection<Class>; classes!: Collection<Class>;
} }

View file

@ -1,9 +1,4 @@
import { import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
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 { LOG_LEVEL, LOKI_HOST } from '../config.js'; import { LOG_LEVEL, LOKI_HOST } from '../config.js';
@ -48,9 +43,7 @@ function initializeLogger(): Logger {
transports: [lokiTransport, consoleTransport], transports: [lokiTransport, consoleTransport],
}); });
logger.debug( logger.debug(`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`);
`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`
);
return logger; return logger;
} }

View file

@ -12,42 +12,28 @@ export class MikroOrmLogger extends DefaultLogger {
switch (namespace) { switch (namespace) {
case 'query': case 'query':
this.logger.debug( this.logger.debug(this.createMessage(namespace, message, context));
this.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.logger.info(this.createMessage(namespace, message, context));
this.createMessage(namespace, message, context)
);
break; break;
case 'schema': case 'schema':
this.logger.info( this.logger.info(this.createMessage(namespace, message, context));
this.createMessage(namespace, message, context)
);
break; break;
case 'discovery': case 'discovery':
this.logger.debug( this.logger.debug(this.createMessage(namespace, message, context));
this.createMessage(namespace, message, context)
);
break; break;
case 'info': case 'info':
this.logger.info( this.logger.info(this.createMessage(namespace, message, context));
this.createMessage(namespace, message, context)
);
break; break;
case 'deprecated': case 'deprecated':
this.logger.warn( this.logger.warn(this.createMessage(namespace, message, context));
this.createMessage(namespace, message, context)
);
break; break;
default: default:
switch (context?.level) { switch (context?.level) {
case 'info': case 'info':
this.logger.info( this.logger.info(this.createMessage(namespace, message, context));
this.createMessage(namespace, message, context)
);
break; break;
case 'warning': case 'warning':
this.logger.warn(message); this.logger.warn(message);
@ -62,11 +48,7 @@ export class MikroOrmLogger extends DefaultLogger {
} }
} }
private createMessage( private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) {
namespace: LoggerNamespace,
messageArg: string,
context?: LogContext
) {
const labels: LokiLabels = { const labels: LokiLabels = {
service: 'ORM', service: 'ORM',
}; };

View file

@ -52,9 +52,7 @@ function config(testingMode: boolean = false): Options {
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION) // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint) // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
dynamicImportProvider: (id) => { dynamicImportProvider: (id) => import(id),
return import(id);
},
}; };
} }
@ -70,9 +68,7 @@ function config(testingMode: boolean = false): Options {
// Logging // Logging
debug: LOG_LEVEL === 'debug', debug: LOG_LEVEL === 'debug',
loggerFactory: (options: LoggerOptions) => { loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
return new MikroOrmLogger(options);
},
}; };
} }

View file

@ -28,9 +28,7 @@ export async function initORM(testingMode: boolean = false) {
} }
export function forkEntityManager(): EntityManager { export function forkEntityManager(): EntityManager {
if (!orm) { if (!orm) {
throw Error( throw Error('Accessing the Entity Manager before the ORM is fully initialized.');
'Accessing the Entity Manager before the ORM is fully initialized.'
);
} }
return orm.em.fork(); return orm.em.fork();
} }

View file

@ -1,8 +1,5 @@
import express from 'express'; import express from 'express';
import { import { getAllLearningObjects, getLearningObject } from '../controllers/learningObjects.js';
getAllLearningObjects,
getLearningObject,
} from '../controllers/learningObjects.js';
const router = express.Router(); const router = express.Router();

View file

@ -15,8 +15,7 @@ router.get('/:id', (req, res) => {
student: '0', student: '0',
group: '0', group: '0',
time: new Date(2025, 1, 1), time: new Date(2025, 1, 1),
content: content: 'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????',
'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????',
learningObject: '0', learningObject: '0',
links: { links: {
self: `${req.baseUrl}/${req.params.id}`, self: `${req.baseUrl}/${req.params.id}`,

View file

@ -1,20 +1,12 @@
import { DWENGO_API_BASE } from '../config.js'; import { DWENGO_API_BASE } from '../config.js';
import { fetchWithLogging } from '../util/apiHelper.js'; import { fetchWithLogging } from '../util/apiHelper.js';
import { import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learningPath.js';
FilteredLearningObject,
LearningObjectMetadata,
LearningObjectNode,
LearningPathResponse,
} from '../interfaces/learningPath.js';
import { fetchLearningPaths } from './learningPaths.js'; import { fetchLearningPaths } from './learningPaths.js';
import { getLogger, Logger } from '../logging/initalize.js'; import { getLogger, Logger } from '../logging/initalize.js';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
function filterData( function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
data: LearningObjectMetadata,
htmlUrl: string
): FilteredLearningObject {
return { return {
key: data.hruid, // Hruid learningObject (not path) key: data.hruid, // Hruid learningObject (not path)
_id: data._id, _id: data._id,
@ -41,10 +33,7 @@ function filterData(
/** /**
* Fetches a single learning object by its HRUID * Fetches a single learning object by its HRUID
*/ */
export async function getLearningObjectById( export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> {
hruid: string,
language: string
): Promise<FilteredLearningObject | null> {
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`;
const metadata = await fetchWithLogging<LearningObjectMetadata>( const metadata = await fetchWithLogging<LearningObjectMetadata>(
metadataUrl, metadataUrl,
@ -63,49 +52,24 @@ export async function getLearningObjectById(
/** /**
* Generic function to fetch learning objects (full data or just HRUIDs) * Generic function to fetch learning objects (full data or just HRUIDs)
*/ */
async function fetchLearningObjects( async function fetchLearningObjects(hruid: string, full: boolean, language: string): Promise<FilteredLearningObject[] | string[]> {
hruid: string,
full: boolean,
language: string
): Promise<FilteredLearningObject[] | string[]> {
try { try {
const learningPathResponse: LearningPathResponse = const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`);
await fetchLearningPaths(
[hruid],
language,
`Learning path for HRUID "${hruid}"`
);
if ( if (!learningPathResponse.success || !learningPathResponse.data?.length) {
!learningPathResponse.success || logger.warn(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
!learningPathResponse.data?.length
) {
logger.warn(
`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`
);
return []; return [];
} }
const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes; const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes;
if (!full) { if (!full) {
return nodes.map((node) => { return nodes.map((node) => node.learningobject_hruid);
return node.learningobject_hruid;
});
} }
return await Promise.all( return await Promise.all(nodes.map(async (node) => getLearningObjectById(node.learningobject_hruid, language))).then((objects) =>
nodes.map(async (node) => { objects.filter((obj): obj is FilteredLearningObject => obj !== null)
return getLearningObjectById( );
node.learningobject_hruid,
language
);
})
).then((objects) => {
return objects.filter((obj): obj is FilteredLearningObject => {
return obj !== null;
});
});
} catch (error) { } catch (error) {
logger.error('❌ Error fetching learning objects:', error); logger.error('❌ Error fetching learning objects:', error);
return []; return [];
@ -115,23 +79,13 @@ async function fetchLearningObjects(
/** /**
* Fetch full learning object data (metadata) * Fetch full learning object data (metadata)
*/ */
export async function getLearningObjectsFromPath( export async function getLearningObjectsFromPath(hruid: string, language: string): Promise<FilteredLearningObject[]> {
hruid: string, return (await fetchLearningObjects(hruid, true, language)) as FilteredLearningObject[];
language: string
): Promise<FilteredLearningObject[]> {
return (await fetchLearningObjects(
hruid,
true,
language
)) as FilteredLearningObject[];
} }
/** /**
* Fetch only learning object HRUIDs * Fetch only learning object HRUIDs
*/ */
export async function getLearningObjectIdsFromPath( export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
hruid: string,
language: string
): Promise<string[]> {
return (await fetchLearningObjects(hruid, false, language)) as string[]; return (await fetchLearningObjects(hruid, false, language)) as string[];
} }

View file

@ -1,18 +1,11 @@
import { fetchWithLogging } from '../util/apiHelper.js'; import { fetchWithLogging } from '../util/apiHelper.js';
import { DWENGO_API_BASE } from '../config.js'; import { DWENGO_API_BASE } from '../config.js';
import { import { LearningPath, LearningPathResponse } from '../interfaces/learningPath.js';
LearningPath,
LearningPathResponse,
} from '../interfaces/learningPath.js';
import { getLogger, Logger } from '../logging/initalize.js'; import { getLogger, Logger } from '../logging/initalize.js';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
export async function fetchLearningPaths( export async function fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> {
hruids: string[],
language: string,
source: string
): Promise<LearningPathResponse> {
if (hruids.length === 0) { if (hruids.length === 0) {
return { return {
success: false, success: false,
@ -25,11 +18,7 @@ export async function fetchLearningPaths(
const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`;
const params = { pathIdList: JSON.stringify({ hruids }), language }; const params = { pathIdList: JSON.stringify({ hruids }), language };
const learningPaths = await fetchWithLogging<LearningPath[]>( const learningPaths = await fetchWithLogging<LearningPath[]>(apiUrl, `Learning paths for ${source}`, params);
apiUrl,
`Learning paths for ${source}`,
params
);
if (!learningPaths || learningPaths.length === 0) { if (!learningPaths || learningPaths.length === 0) {
logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`); logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`);
@ -48,17 +37,10 @@ export async function fetchLearningPaths(
}; };
} }
export async function searchLearningPaths( export async function searchLearningPaths(query: string, language: string): Promise<LearningPath[]> {
query: string,
language: string
): Promise<LearningPath[]> {
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
const params = { all: query, language }; const params = { all: query, language };
const searchResults = await fetchWithLogging<LearningPath[]>( const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, params);
apiUrl,
`Search learning paths with query "${query}"`,
params
);
return searchResults ?? []; return searchResults ?? [];
} }

View file

@ -12,11 +12,7 @@ const logger: Logger = getLogger();
* @param params * @param params
* @returns The response data if successful, or null if an error occurs. * @returns The response data if successful, or null if an error occurs.
*/ */
export async function fetchWithLogging<T>( export async function fetchWithLogging<T>(url: string, description: string, params?: Record<string, any>): Promise<T | null> {
url: string,
description: string,
params?: Record<string, any>
): Promise<T | null> {
try { try {
const config: AxiosRequestConfig = params ? { params } : {}; const config: AxiosRequestConfig = params ? { params } : {};
@ -25,19 +21,14 @@ export async function fetchWithLogging<T>(
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
if (error.response.status === 404) { if (error.response.status === 404) {
logger.debug( logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`);
`❌ ERROR: ${description} not found (404) at "${url}".`
);
} else { } else {
logger.debug( logger.debug(
`❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")`
); );
} }
} else { } else {
logger.debug( logger.debug(`❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message);
`❌ ERROR: Network or unexpected error when fetching ${description}:`,
error.message
);
} }
return null; return null;
} }

View file

@ -36,9 +36,7 @@ export function getNumericEnvVar(envVar: EnvVar): number {
const valueString = getEnvVar(envVar); const valueString = getEnvVar(envVar);
const value = parseInt(valueString); const value = parseInt(valueString);
if (isNaN(value)) { if (isNaN(value)) {
throw new Error( throw new Error(`Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.`);
`Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.`
);
} else { } else {
return value; return value;
} }

View file

@ -1,7 +1,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { FALLBACK_LANG } from '../../config.js'; import { FALLBACK_LANG } from '../config.js';
import { getLogger, Logger } from '../logging/initalize.js'; import { getLogger, Logger } from '../logging/initalize.js';
const logger: Logger = getLogger(); const logger: Logger = getLogger();
@ -12,15 +12,8 @@ export function loadTranslations<T>(language: string): T {
const yamlFile = fs.readFileSync(filePath, 'utf8'); const yamlFile = fs.readFileSync(filePath, 'utf8');
return yaml.load(yamlFile) as T; return yaml.load(yamlFile) as T;
} catch (error) { } catch (error) {
logger.warn( logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error);
`Cannot load translation for ${language}, fallen back to dutch`, const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`);
error
);
const fallbackPath = path.join(
process.cwd(),
'_i18n',
`${FALLBACK_LANG}.yml`
);
return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T; return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T;
} }
} }

View file

@ -16,12 +16,9 @@ describe('StudentRepository', () => {
}); });
it('should return the queried student after he was added', async () => { it('should return the queried student after he was added', async () => {
await studentRepository.insert( await studentRepository.insert(new Student(username, firstName, lastName));
new Student(username, firstName, lastName)
);
const retrievedStudent = const retrievedStudent = await studentRepository.findByUsername(username);
await studentRepository.findByUsername(username);
expect(retrievedStudent).toBeTruthy(); expect(retrievedStudent).toBeTruthy();
expect(retrievedStudent?.firstName).toBe(firstName); expect(retrievedStudent?.firstName).toBe(firstName);
expect(retrievedStudent?.lastName).toBe(lastName); expect(retrievedStudent?.lastName).toBe(lastName);
@ -30,8 +27,7 @@ describe('StudentRepository', () => {
it('should no longer return the queried student after he was removed again', async () => { it('should no longer return the queried student after he was removed again', async () => {
await studentRepository.deleteByUsername(username); await studentRepository.deleteByUsername(username);
const retrievedStudent = const retrievedStudent = await studentRepository.findByUsername(username);
await studentRepository.findByUsername(username);
expect(retrievedStudent).toBeNull(); expect(retrievedStudent).toBeNull();
}); });
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Before After
Before After

View file

@ -1,30 +1,49 @@
from diagrams import Cluster, Diagram from diagrams import Cluster, Diagram, Edge
from diagrams.custom import Custom from diagrams.custom import Custom
from diagrams.onprem.certificates import LetsEncrypt from diagrams.onprem.certificates import LetsEncrypt
from diagrams.onprem.container import Docker
from diagrams.onprem.database import PostgreSQL from diagrams.onprem.database import PostgreSQL
from diagrams.onprem.logging import Loki from diagrams.onprem.logging import Loki
from diagrams.onprem.monitoring import Grafana from diagrams.onprem.monitoring import Grafana
from diagrams.onprem.network import Nginx from diagrams.onprem.network import Nginx
from diagrams.programming.flowchart import InputOutput
from diagrams.programming.framework import Vue from diagrams.programming.framework import Vue
from diagrams.programming.language import Nodejs from diagrams.programming.language import Nodejs
from diagrams.programming.flowchart import InputOutput
with Diagram("Dwengo-1 architectuur", filename="docs/architecture/schema", show=False): with Diagram("Dwengo-1 architectuur", filename="docs/architecture/schema", show=False):
reverse_proxy = Nginx("reverse proxy") ingress = Nginx("Reverse Proxy")
reverse_proxy >> LetsEncrypt("SSL") certificates = LetsEncrypt("SSL")
with Cluster("Docker"): with Cluster("Dwengo VZW"):
Docker()
frontend = Vue("/")
backend = Nodejs("/api")
reverse_proxy >> frontend
frontend >> backend >> InputOutput("MikroORM") >> PostgreSQL()
backend >> Loki("logging") >> Grafana("monitoring")
with Cluster("Dwengo"):
dwengo = Custom("Dwengo", "../../assets/img/dwengo-groen-zwart.png") dwengo = Custom("Dwengo", "../../assets/img/dwengo-groen-zwart.png")
backend >> dwengo with Cluster("Dwengo-1"):
frontend = Vue("/")
backend = Nodejs("/api")
identity_provider = Custom("IDP", "../../assets/img/keycloak.png")
database = PostgreSQL("Database")
orm = InputOutput("MikroORM")
orm >> Edge(label="map") << database
with Cluster("Observability"):
logging = Loki("Logging")
logging << Edge(color="firebrick", style="dashed") << Grafana("Monitoring")
dependencies = [
dwengo,
logging,
orm
]
backend >> dependencies
service = [
frontend,
backend,
identity_provider,
certificates
]
ingress \
>> Edge(color="darkgreen") \
<< service

View file

@ -16,12 +16,7 @@ export default [
prettierConfig, prettierConfig,
includeIgnoreFile(gitignorePath), includeIgnoreFile(gitignorePath),
{ {
ignores: [ ignores: ['**/dist/**', '**/.node_modules/**', '**/coverage/**', '**/.github/**'],
'**/dist/**',
'**/.node_modules/**',
'**/coverage/**',
'**/.github/**',
],
files: ['**/*.ts', '**/*.cts', '**.*.mts', '**/*.ts'], files: ['**/*.ts', '**/*.cts', '**.*.mts', '**/*.ts'],
}, },
{ {
@ -43,8 +38,9 @@ export default [
'no-unreachable-loop': 'warn', 'no-unreachable-loop': 'warn',
'no-use-before-define': 'error', 'no-use-before-define': 'error',
'no-useless-assignment': 'error', 'no-useless-assignment': 'error',
'no-unused-vars': 'error',
'arrow-body-style': ['warn', 'always'], 'arrow-body-style': ['warn', 'as-needed'],
'block-scoped-var': 'warn', 'block-scoped-var': 'warn',
camelcase: 'warn', camelcase: 'warn',
'capitalized-comments': 'warn', 'capitalized-comments': 'warn',

View file

@ -14,6 +14,9 @@ const vueConfig = defineConfigWithVueTs(
{ {
name: "app/files-to-lint", name: "app/files-to-lint",
files: ["**/*.{ts,mts,tsx,vue}"], files: ["**/*.{ts,mts,tsx,vue}"],
rules: {
"no-useless-assignment": "off", // Depend on `no-unused-vars` to catch this
},
}, },
{ {

View file

@ -22,16 +22,12 @@ const router = createRouter({
{ {
path: "/", path: "/",
name: "home", name: "home",
component: () => { component: () => import("../views/HomePage.vue"),
return import("../views/HomePage.vue");
},
}, },
{ {
path: "/login", path: "/login",
name: "LoginPage", name: "LoginPage",
component: () => { component: () => import("../views/LoginPage.vue"),
return import("../views/LoginPage.vue");
},
}, },
{ {
path: "/student/:id", path: "/student/:id",

View file

@ -2,7 +2,7 @@
* @type {import("prettier").Options} * @type {import("prettier").Options}
*/ */
export default { export default {
printWidth: 80, printWidth: 150,
semi: true, semi: true,
singleQuote: true, singleQuote: true,
trailingComma: 'es5', trailingComma: 'es5',