Merge branch 'dev' into refactor/common

This commit is contained in:
Tibo De Peuter 2025-04-01 21:38:50 +02:00
commit a4408b5bc0
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
145 changed files with 3437 additions and 2822 deletions

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

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

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

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

View file

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

View file

@ -11,7 +11,7 @@
"format": "prettier --write src/",
"format-check": "prettier --check src/",
"lint": "eslint . --fix",
"test:unit": "vitest"
"test:unit": "vitest --run"
},
"dependencies": {
"@mikro-orm/core": "6.4.9",

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmi
import { AssignmentDTO } from 'dwengo-1-common/src/interfaces/assignment';
// Typescript is annoy with with parameter forwarding from class.ts
// Typescript is annoying with parameter forwarding from class.ts
interface AssignmentParams {
classid: string;
id: string;
@ -42,7 +42,7 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re
}
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const id = +req.params.id;
const id = Number(req.params.id);
const classid = req.params.classid;
if (isNaN(id)) {
@ -62,7 +62,7 @@ export async function getAssignmentHandler(req: Request<AssignmentParams>, res:
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentNumber = +req.params.id;
const assignmentNumber = Number(req.params.id);
const full = req.query.full === 'true';
if (isNaN(assignmentNumber)) {

View file

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

View file

@ -13,14 +13,14 @@ interface GroupParams {
export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> {
const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid;
const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groupId = +req.params.groupid!; // Can't be undefined
const groupId = Number(req.params.groupid!); // Can't be undefined
if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' });
@ -41,7 +41,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid;
const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
@ -57,7 +57,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise<
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid;
const assignmentId = +req.params.assignmentid;
const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
@ -79,14 +79,14 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P
const classId = req.params.classid;
const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid;
const assignmentId = Number(req.params.assignmentid);
if (isNaN(assignmentId)) {
res.status(400).json({ error: 'Assignment id must be a number' });
return;
}
const groupId = +req.params.groupid!; // Can't be undefined
const groupId = Number(req.params.groupid); // Can't be undefined
if (isNaN(groupId)) {
res.status(400).json({ error: 'Group id must be a number' });

View file

@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import { FALLBACK_LANG } from '../config.js';
import learningObjectService from '../services/learning-objects/learning-object-service.js';
import { EnvVars, getEnvVar } from '../util/envvars.js';
import { envVars, getEnvVar } from '../util/envVars.js';
import attachmentService from '../services/learning-objects/attachment-service.js';
import { NotFoundError } from '@mikro-orm/core';
import { BadRequestException } from '../exceptions/bad-request-exception.js';
@ -13,8 +13,8 @@ function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIde
throw new BadRequestException('HRUID is required.');
}
return {
hruid: req.params.hruid as string,
language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language,
hruid: req.params.hruid,
language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language,
version: parseInt(req.query.version as string),
};
}
@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif
throw new BadRequestException('HRUID is required.');
}
return {
hruid: req.params.hruid as string,
hruid: req.params.hruid,
language: (req.query.language as Language) || FALLBACK_LANG,
};
}

View file

@ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu
return {
hruid,
language: (lang as Language) || FALLBACK_LANG,
version: +version,
version: Number(version),
};
}

View file

@ -47,7 +47,7 @@ export async function getStudentHandler(req: Request, res: Response): Promise<vo
res.json(user);
}
export async function createStudentHandler(req: Request, res: Response) {
export async function createStudentHandler(req: Request, res: Response): Promise<void> {
const userData = req.body as StudentDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
@ -69,7 +69,7 @@ export async function createStudentHandler(req: Request, res: Response) {
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;
if (!username) {
@ -94,9 +94,7 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro
const classes = await getStudentClasses(username, full);
res.json({
classes: classes,
});
res.json({ classes: classes });
}
// TODO

View file

@ -11,7 +11,7 @@ interface SubmissionParams {
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> {
const lohruid = req.params.hruid;
const submissionNumber = +req.params.id;
const submissionNumber = Number(req.params.id);
if (isNaN(submissionNumber)) {
res.status(400).json({ error: 'Submission number is not a number' });
@ -31,7 +31,7 @@ export async function getSubmissionHandler(req: Request<SubmissionParams>, res:
res.json(submission);
}
export async function createSubmissionHandler(req: Request, res: Response) {
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
const submissionDTO = req.body as SubmissionDTO;
const submission = await createSubmission(submissionDTO);
@ -44,9 +44,9 @@ export async function createSubmissionHandler(req: Request, res: Response) {
res.json(submission);
}
export async function deleteSubmissionHandler(req: Request, res: Response) {
export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
const hruid = req.params.hruid;
const submissionNumber = +req.params.id;
const submissionNumber = Number(req.params.id);
const lang = languageMap[req.query.language as string] || Language.Dutch;
const version = (req.query.version || 1) as number;

View file

@ -44,7 +44,7 @@ export async function getTeacherHandler(req: Request, res: Response): Promise<vo
res.json(user);
}
export async function createTeacherHandler(req: Request, res: Response) {
export async function createTeacherHandler(req: Request, res: Response): Promise<void> {
const userData = req.body as TeacherDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
@ -64,7 +64,7 @@ export async function createTeacherHandler(req: Request, res: Response) {
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;
if (!username) {
@ -84,7 +84,7 @@ export async function deleteTeacherHandler(req: Request, res: Response) {
}
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username as string;
const username = req.params.username;
const full = req.query.full === 'true';
if (!username) {
@ -103,7 +103,7 @@ export async function getTeacherClassHandler(req: Request, res: Response): Promi
}
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username as string;
const username = req.params.username;
const full = req.query.full === 'true';
if (!username) {
@ -122,7 +122,7 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro
}
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';
if (!username) {

View file

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

View file

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

View file

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

View file

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

View file

@ -4,13 +4,13 @@ import { ClassJoinRequest } from '../../entities/classes/class-join-request.enti
import { Student } from '../../entities/users/student.entity.js';
export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { requester: requester } });
}
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { class: clazz } });
}
public deleteBy(requester: Student, clazz: Class): Promise<void> {
public async deleteBy(requester: Student, clazz: Class): Promise<void> {
return this.deleteWhere({ requester: requester, class: clazz });
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from 'dwengo-1-common/src/util/language.js';
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'] });
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,7 +16,7 @@ export class Submission {
learningObjectLanguage!: Language;
@PrimaryKey({ type: 'numeric' })
learningObjectVersion: number = 1;
learningObjectVersion = 1;
@PrimaryKey({ type: 'integer', autoincrement: true })
submissionNumber?: number;

View file

@ -0,0 +1,10 @@
import { Embeddable, Property } from '@mikro-orm/core';
@Embeddable()
export class EducationalGoal {
@Property({ type: 'string' })
source!: string;
@Property({ type: 'string' })
id!: string;
}

View file

@ -5,5 +5,7 @@ export class LearningObjectIdentifier {
public hruid: string,
public language: Language,
public version: number
) {}
) {
// Do nothing
}
}

View file

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

View file

@ -0,0 +1,10 @@
import { Embeddable, Property } from '@mikro-orm/core';
@Embeddable()
export class ReturnValue {
@Property({ type: 'string' })
callbackUrl!: string;
@Property({ type: 'json' })
callbackSchema!: string;
}

View file

@ -15,7 +15,7 @@ export class Question {
learningObjectLanguage!: Language;
@PrimaryKey({ type: 'number' })
learningObjectVersion: number = 1;
learningObjectVersion = 1;
@PrimaryKey({ type: 'integer', autoincrement: true })
sequenceNumber?: number;

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import { languageMap } from 'dwengo-1-common/src/util/language.js';
import { FALLBACK_LANG } from '../config.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { Class } from '../entities/classes/class.entity.js';
import { getLogger } from '../logging/initalize.js';
import { AssignmentDTO } from 'dwengo-1-common/src/interfaces/assignment';
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO {
@ -36,5 +37,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
assignment.within = cls;
getLogger().debug(assignment);
return assignment;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ router.get('/:groupid', getGroupHandler);
router.get('/:groupid', getGroupSubmissionsHandler);
// The list of questions a group has made
router.get('/:id/questions', (req, res) => {
router.get('/:id/questions', (_req, res) => {
res.json({
questions: ['0'],
});

View file

@ -9,6 +9,7 @@ import {
getStudentHandler,
getStudentSubmissionsHandler,
} from '../controllers/students.js';
const router = express.Router();
// Root endpoint used to search objects
@ -36,7 +37,7 @@ router.get('/:id/assignments', getStudentAssignmentsHandler);
router.get('/:id/groups', getStudentGroupsHandler);
// A list of questions a user has created
router.get('/:id/questions', (req, res) => {
router.get('/:id/questions', (_req, res) => {
res.json({
questions: ['0'],
});

View file

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

View file

@ -28,7 +28,7 @@ router.get('/:username/students', getTeacherStudentHandler);
router.get('/:username/questions', getTeacherQuestionHandler);
// Invitations to other classes a teacher received
router.get('/:id/invitations', (req, res) => {
router.get('/:id/invitations', (_req, res) => {
res.json({
invitations: ['0'],
});

View file

@ -3,6 +3,7 @@ import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../in
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
import { AssignmentDTO } from 'dwengo-1-common/src/interfaces/assignment';
import { SubmissionDTO, SubmissionDTOId } from 'dwengo-1-common/src/interfaces/submission';
import { getLogger } from '../logging/initalize.js';
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
const classRepository = getClassRepository();
@ -39,7 +40,7 @@ export async function createAssignment(classid: string, assignmentData: Assignme
return mapToAssignmentDTO(newAssignment);
} catch (e) {
console.error(e);
getLogger().error(e);
return null;
}
}
@ -85,7 +86,7 @@ export async function getAssignmentsSubmissions(
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
const submissionRepository = getSubmissionRepository();
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
if (full) {
return submissions.map(mapToSubmissionDTO);

View file

@ -26,11 +26,15 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> {
const teacherRepository = getTeacherRepository();
const teacherUsernames = classData.teachers || [];
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null);
const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter(
(teacher) => teacher !== null
);
const studentRepository = getStudentRepository();
const studentUsernames = classData.students || [];
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter(
(student) => student !== null
);
const classRepository = getClassRepository();

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import { Attachment } from '../../entities/content/attachment.entity.js';
import { LearningObjectIdentifier } from 'dwengo-1-common/src/interfaces/learning-content';
const attachmentService = {
getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
const attachmentRepo = getAttachmentRepository();
if (learningObjectId.version) {

View file

@ -41,10 +41,10 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
};
}
function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise<LearningObject | null> {
const learningObjectRepo = getLearningObjectRepository();
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language);
return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
}
/**
@ -65,11 +65,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
const learningObjectRepo = getLearningObjectRepository();
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language);
const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language);
if (!learningObject) {
return null;
}
return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id));
return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id));
},
/**
@ -96,7 +96,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = {
throw new NotFoundError('The learning path with the given ID could not be found.');
}
const learningObjects = await Promise.all(
learningPath.nodes.map((it) => {
learningPath.nodes.map(async (it) => {
const learningObject = learningObjectService.getLearningObjectById({
hruid: it.learningObjectHruid,
language: it.language,

View file

@ -90,7 +90,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
metadataUrl,
`Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`,
{
params: id,
params: { ...id },
}
);
@ -123,7 +123,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = {
async getLearningObjectHTML(id: LearningObjectIdentifier): Promise<string | null> {
const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`;
const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, {
params: id,
params: { ...id },
});
if (!html) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ class BlockImageProcessor extends InlineImageProcessor {
super();
}
override renderFn(imageUrl: string) {
override renderFn(imageUrl: string): string {
const inlineHtml = super.render(imageUrl);
return DOMPurify.sanitize(`<div>${inlineHtml}</div>`);
}

View file

@ -13,7 +13,7 @@ class InlineImageProcessor extends StringProcessor {
super(contentType);
}
override renderFn(imageUrl: string) {
override renderFn(imageUrl: string): string {
if (!isValidHttpUrl(imageUrl)) {
throw new ProcessingError(`Image URL is invalid: ${imageUrl}`);
}

View file

@ -14,26 +14,24 @@ class MarkdownProcessor extends StringProcessor {
super(DwengoContentType.TEXT_MARKDOWN);
}
override renderFn(mdText: string) {
let html = '';
try {
marked.use({ renderer: dwengoMarkedRenderer });
html = marked(mdText, { async: false });
html = this.replaceLinks(html); // Replace html image links path
} catch (e: any) {
throw new ProcessingError(e.message);
}
return html;
}
replaceLinks(html: string) {
static replaceLinks(html: string): string {
const proc = new InlineImageProcessor();
html = html.replace(
/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g,
(match: string, src: string, alt: string, altText: string, title: string, titleText: string) => proc.render(src)
(_match: string, src: string, _alt: string, _altText: string, _title: string, _titleText: string) => proc.render(src)
);
return html;
}
override renderFn(mdText: string): string {
try {
marked.use({ renderer: dwengoMarkedRenderer });
const html = marked(mdText, { async: false });
return MarkdownProcessor.replaceLinks(html); // Replace html image links path
} catch (e: unknown) {
throw new ProcessingError('Unknown error while processing markdown: ' + e);
}
}
}
export { MarkdownProcessor };

View file

@ -15,7 +15,7 @@ class PdfProcessor extends StringProcessor {
super(DwengoContentType.APPLICATION_PDF);
}
override renderFn(pdfUrl: string) {
override renderFn(pdfUrl: string): string {
if (!isValidHttpUrl(pdfUrl)) {
throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`);
}

View file

@ -21,7 +21,7 @@ const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" l
const LEARNING_OBJECT_DOES_NOT_EXIST = "<div class='non-existing-learning-object' />";
class ProcessingService {
private processors!: Map<DwengoContentType, Processor<any>>;
private processors!: Map<DwengoContentType, Processor<DwengoContentType>>;
constructor() {
const processors = [

View file

@ -9,7 +9,9 @@ import { DwengoContentType } from './content-type.js';
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processor.js
*/
abstract class Processor<T> {
protected constructor(public contentType: DwengoContentType) {}
protected constructor(public contentType: DwengoContentType) {
// Do nothing
}
/**
* Render the given object.

View file

@ -11,7 +11,7 @@ class TextProcessor extends StringProcessor {
super(DwengoContentType.TEXT_PLAIN);
}
override renderFn(text: string) {
override renderFn(text: string): string {
// Sanitize plain text to prevent xss.
return DOMPurify.sanitize(text);
}

View file

@ -24,14 +24,14 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
// Its corresponding learning object.
const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>(
await Promise.all(
nodes.map((node) =>
nodes.map(async (node) =>
learningObjectService
.getLearningObjectById({
hruid: node.learningObjectHruid,
version: node.version,
language: node.language,
})
.then((learningObject) => <[LearningPathNode, FilteredLearningObject | null]>[node, learningObject])
.then((learningObject) => [node, learningObject] as [LearningPathNode, FilteredLearningObject | null])
)
)
);
@ -123,7 +123,7 @@ async function convertNodes(
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>,
personalizedFor?: PersonalizationTarget
): Promise<LearningObjectNode[]> {
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map((entry) =>
const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) =>
convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects)
);
return await Promise.all(nodesPromise);
@ -158,7 +158,7 @@ function convertTransition(
throw new Error(`Learning object ${transition.next.learningObjectHruid}/${transition.next.language}/${transition.next.version} not found!`);
} else {
return {
_id: '' + index, // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
_id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
default: false, // We don't work with default transitions but retain this for backwards compatibility.
next: {
_id: nextNode._id + index, // Construct a unique ID for the transition for backwards compatibility.
@ -185,11 +185,11 @@ const databaseLearningPathProvider: LearningPathProvider = {
): Promise<LearningPathResponse> {
const learningPathRepo = getLearningPathRepository();
const learningPaths = (await Promise.all(hruids.map((hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter(
const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter(
(learningPath) => learningPath !== null
);
const filteredLearningPaths = await Promise.all(
learningPaths.map((learningPath, index) => convertLearningPath(learningPath, index, personalizedFor))
learningPaths.map(async (learningPath, index) => convertLearningPath(learningPath, index, personalizedFor))
);
return {
@ -206,7 +206,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
const learningPathRepo = getLearningPathRepository();
const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language);
return await Promise.all(searchResults.map((result, index) => convertLearningPath(result, index, personalizedFor)));
return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor)));
},
};

View file

@ -1,11 +1,11 @@
import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js';
import databaseLearningPathProvider from './database-learning-path-provider.js';
import { EnvVars, getEnvVar } from '../../util/envvars.js';
import { envVars, getEnvVar } from '../../util/envVars.js';
import { PersonalizationTarget } from './learning-path-personalization-util.js';
import { LearningPath, LearningPathResponse } from 'dwengo-1-common/src/interfaces/learning-content';
import { Language } from 'dwengo-1-common/src/util/language.js';
const userContentPrefix = getEnvVar(EnvVars.UserContentPrefix);
const userContentPrefix = getEnvVar(envVars.UserContentPrefix);
const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider];
/**
@ -49,7 +49,9 @@ const learningPathService = {
* Search learning paths in the data source using the given search string.
*/
async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> {
const providerResponses = await Promise.all(allProviders.map((provider) => provider.searchLearningPaths(query, language, personalizedFor)));
const providerResponses = await Promise.all(
allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor))
);
return providerResponses.flat();
},
};

View file

@ -5,10 +5,9 @@ import { Answer } from '../entities/questions/answer.entity.js';
import { mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js';
import { QuestionRepository } from '../data/questions/question-repository.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToUser } from '../interfaces/user.js';
import { Student } from '../entities/users/student.entity.js';
import { mapToStudent } from '../interfaces/student.js';
import { QuestionDTO, QuestionId } from 'dwengo-1-common/src/interfaces/question';
import { AnswerDTO, AnswerId } from 'dwengo-1-common/src/interfaces/answer';
export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const questionRepository: QuestionRepository = getQuestionRepository();
@ -48,7 +47,7 @@ export async function getQuestion(questionId: QuestionId): Promise<QuestionDTO |
return mapToQuestionDTO(question);
}
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean) {
export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise<AnswerDTO[] | AnswerId[]> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
@ -71,7 +70,7 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean
return answersDTO.map(mapToAnswerId);
}
export async function createQuestion(questionDTO: QuestionDTO) {
export async function createQuestion(questionDTO: QuestionDTO): Promise<QuestionDTO | null> {
const questionRepository = getQuestionRepository();
const author = mapToStudent(questionDTO.author);
@ -82,14 +81,14 @@ export async function createQuestion(questionDTO: QuestionDTO) {
author,
content: questionDTO.content,
});
} catch (e) {
} catch (_) {
return null;
}
return questionDTO;
}
export async function deleteQuestion(questionId: QuestionId) {
export async function deleteQuestion(questionId: QuestionId): Promise<QuestionDTO | null> {
const questionRepository = getQuestionRepository();
const question = await fetchQuestion(questionId);
@ -100,7 +99,7 @@ export async function deleteQuestion(questionId: QuestionId) {
try {
await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(questionId.learningObjectIdentifier, questionId.sequenceNumber);
} catch (e) {
} catch (_) {
return null;
}

View file

@ -9,6 +9,7 @@ import { ClassDTO } from 'dwengo-1-common/src/interfaces/class';
import { GroupDTO } from 'dwengo-1-common/src/interfaces/group';
import { SubmissionDTO, SubmissionDTOId } from 'dwengo-1-common/src/interfaces/submission';
import { StudentDTO } from 'dwengo-1-common/src/interfaces/student';
import { getLogger } from '../logging/initalize.js';
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
const studentRepository = getStudentRepository();
@ -49,7 +50,7 @@ export async function deleteStudent(username: string): Promise<StudentDTO | null
return mapToStudentDTO(user);
} catch (e) {
console.log(e);
getLogger().error(e);
return null;
}
}

View file

@ -22,21 +22,26 @@ export async function getSubmission(
return mapToSubmissionDTO(submission);
}
export async function createSubmission(submissionDTO: SubmissionDTO) {
export async function createSubmission(submissionDTO: SubmissionDTO): Promise<SubmissionDTO | null> {
const submissionRepository = getSubmissionRepository();
const submission = mapToSubmission(submissionDTO);
try {
const newSubmission = await submissionRepository.create(submission);
const newSubmission = submissionRepository.create(submission);
await submissionRepository.save(newSubmission);
} catch (e) {
} catch (_) {
return null;
}
return mapToSubmissionDTO(submission);
}
export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) {
export async function deleteSubmission(
learningObjectHruid: string,
language: Language,
version: number,
submissionNumber: number
): Promise<SubmissionDTO | null> {
const submissionRepository = getSubmissionRepository();
const submission = getSubmission(learningObjectHruid, language, version, submissionNumber);

View file

@ -7,6 +7,7 @@ import { ClassDTO } from 'dwengo-1-common/src/interfaces/class';
import { TeacherDTO } from 'dwengo-1-common/src/interfaces/teacher';
import { StudentDTO } from 'dwengo-1-common/src/interfaces/student';
import { QuestionDTO, QuestionId } from 'dwengo-1-common/src/interfaces/question';
import { getLogger } from '../logging/initalize.js';
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
const teacherRepository = getTeacherRepository();
@ -48,7 +49,7 @@ export async function deleteTeacher(username: string): Promise<TeacherDTO | null
return mapToTeacherDTO(user);
} catch (e) {
console.log(e);
getLogger().error(e);
return null;
}
}

View file

@ -14,7 +14,7 @@ import { EntityProperty, EventArgs, EventSubscriber } from '@mikro-orm/core';
* the sequence number will not be filled in.
*/
export class SqliteAutoincrementSubscriber implements EventSubscriber {
private sequenceNumbersForEntityType: Map<string, number> = new Map();
private sequenceNumbersForEntityType = new Map<string, number>();
/**
* When an entity with an autoincremented property which is part of the composite private key is created,
@ -27,14 +27,14 @@ export class SqliteAutoincrementSubscriber implements EventSubscriber {
for (const prop of Object.values(args.meta.properties)) {
const property = prop as EntityProperty<T>;
if (property.primary && property.autoincrement && !(args.entity as Record<string, any>)[property.name]) {
if (property.primary && property.autoincrement && !(args.entity as Record<string, unknown>)[property.name]) {
// Obtain and increment sequence number of this entity.
const propertyKey = args.meta.class.name + '.' + property.name;
const nextSeqNumber = this.sequenceNumbersForEntityType.get(propertyKey) || 0;
this.sequenceNumbersForEntityType.set(propertyKey, nextSeqNumber + 1);
// Set the property accordingly.
(args.entity as Record<string, any>)[property.name] = nextSeqNumber + 1;
(args.entity as Record<string, unknown>)[property.name] = nextSeqNumber + 1;
}
}
}

View file

@ -1,5 +1,6 @@
import axios, { AxiosRequestConfig } from 'axios';
import { getLogger, Logger } from '../logging/initalize.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
const logger: Logger = getLogger();
@ -17,8 +18,8 @@ export async function fetchWithLogging<T>(
url: string,
description: string,
options?: {
params?: Record<string, any>;
query?: Record<string, any>;
params?: Record<string, unknown> | LearningObjectIdentifier;
query?: Record<string, unknown>;
responseType?: 'json' | 'text';
}
): Promise<T | null> {
@ -26,18 +27,21 @@ export async function fetchWithLogging<T>(
const config: AxiosRequestConfig = options || {};
const response = await axios.get<T>(url, config);
return response.data;
} catch (error: any) {
if (error.response) {
if (error.response.status === 404) {
logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`);
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
if (error.response) {
if (error.response.status === 404) {
logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`);
} else {
logger.debug(
`❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")`
);
}
} else {
logger.debug(
`❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")`
);
logger.debug(`❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message);
}
} else {
logger.debug(`❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message);
}
logger.error(`❌ ERROR: Unknown error while fetching ${description}.`, error);
return null;
}
}

View file

@ -6,7 +6,11 @@
* @param regex
* @param replacementFn
*/
export async function replaceAsync(str: string, regex: RegExp, replacementFn: (match: string, ...args: string[]) => Promise<string>) {
export async function replaceAsync(
str: string,
regex: RegExp,
replacementFn: (match: string, ...args: string[]) => Promise<string>
): Promise<string> {
const promises: Promise<string>[] = [];
// First run through matches: add all Promises resulting from the replacement function

View file

@ -6,9 +6,13 @@ const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_';
const CORS_PREFIX = PREFIX + 'CORS_';
const LOGGING_PREFIX = PREFIX + 'LOGGING_';
type EnvVar = { key: string; required?: boolean; defaultValue?: any };
interface EnvVar {
key: string;
required?: boolean;
defaultValue?: number | string | boolean;
}
export const EnvVars: { [key: string]: EnvVar } = {
export const envVars: Record<string, EnvVar> = {
Port: { key: PREFIX + 'PORT', defaultValue: 3000 },
LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' },
FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' },
@ -52,7 +56,7 @@ export function getEnvVar(envVar: EnvVar): string {
} else if (envVar.required) {
throw new Error(`Missing environment variable: ${envVar.key}`);
} else {
return envVar.defaultValue || '';
return String(envVar.defaultValue) || '';
}
}

View file

@ -4,12 +4,12 @@ export function isValidHttpUrl(url: string): boolean {
try {
const parsedUrl = new URL(url, 'http://test.be');
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
} catch (e) {
} catch (_) {
return false;
}
}
export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifier) {
export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifier): string {
let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`;
if (learningObjectId.version) {
url += `&version=${learningObjectId.version}`;

View file

@ -3,9 +3,7 @@ import { setupTestApp } from '../../setup-tests';
import { ClassJoinRequestRepository } from '../../../src/data/classes/class-join-request-repository';
import { getClassJoinRequestRepository, getClassRepository, getStudentRepository } from '../../../src/data/repositories';
import { StudentRepository } from '../../../src/data/users/student-repository';
import { Class } from '../../../src/entities/classes/class.entity';
import { ClassRepository } from '../../../src/data/classes/class-repository';
import { Student } from '../../../src/entities/users/student.entity';
describe('ClassJoinRequestRepository', () => {
let classJoinRequestRepository: ClassJoinRequestRepository;

View file

@ -10,9 +10,12 @@ import { LearningObjectIdentifier } from '../../../src/entities/content/learning
const NEWER_TEST_SUFFIX = 'nEweR';
function createTestLearningObjects(learningObjectRepo: LearningObjectRepository): { older: LearningObject; newer: LearningObject } {
async function createTestLearningObjects(learningObjectRepo: LearningObjectRepository): Promise<{
older: LearningObject;
newer: LearningObject;
}> {
const olderExample = example.createLearningObject();
learningObjectRepo.save(olderExample);
await learningObjectRepo.save(olderExample);
const newerExample = example.createLearningObject();
newerExample.title = 'Newer example';
@ -32,23 +35,21 @@ describe('AttachmentRepository', () => {
beforeAll(async () => {
await setupTestApp();
attachmentRepo = getAttachmentRepository();
exampleLearningObjects = createTestLearningObjects(getLearningObjectRepository());
exampleLearningObjects = await createTestLearningObjects(getLearningObjectRepository());
});
it('can add attachments to learning objects without throwing an error', () => {
it('can add attachments to learning objects without throwing an error', async () => {
attachmentsOlderLearningObject = Object.values(example.createAttachment).map((fn) => fn(exampleLearningObjects.older));
for (const attachment of attachmentsOlderLearningObject) {
attachmentRepo.save(attachment);
}
await Promise.all(attachmentsOlderLearningObject.map(async (attachment) => attachmentRepo.save(attachment)));
});
let attachmentOnlyNewer: Attachment;
it('allows us to add attachments with the same name to a different learning object without throwing an error', () => {
it('allows us to add attachments with the same name to a different learning object without throwing an error', async () => {
attachmentOnlyNewer = Object.values(example.createAttachment)[0](exampleLearningObjects.newer);
attachmentOnlyNewer.content.write(NEWER_TEST_SUFFIX);
attachmentRepo.save(attachmentOnlyNewer);
await attachmentRepo.save(attachmentOnlyNewer);
});
let olderLearningObjectId: LearningObjectIdentifier;

View file

@ -10,7 +10,7 @@ import { Language } from 'dwengo-1-common/src/util/language';
function expectToHaveFoundPrecisely(expected: LearningPath, result: LearningPath[]): void {
expect(result).toHaveProperty('length');
expect(result.length).toBe(1);
expectToBeCorrectEntity({ entity: result[0]! }, { entity: expected });
expectToBeCorrectEntity({ entity: result[0] }, { entity: expected });
}
function expectToHaveFoundNothing(result: LearningPath[]): void {

View file

@ -23,9 +23,9 @@ describe('AnswerRepository', () => {
const id = new LearningObjectIdentifier('id05', Language.English, 1);
const questions = await questionRepository.findAllQuestionsAboutLearningObject(id);
const question = questions.filter((it) => it.sequenceNumber == 2)[0];
const question = questions.find((it) => it.sequenceNumber === 2);
const answers = await answerRepository.findAllAnswersToQuestion(question);
const answers = await answerRepository.findAllAnswersToQuestion(question!);
expect(answers).toBeTruthy();
expect(answers).toHaveLength(2);

View file

@ -2,21 +2,18 @@ import { beforeAll, describe, expect, it } from 'vitest';
import { Language } from 'dwengo-1-common/src/util/language';
import { setupTestApp } from '../../setup-tests';
import { QuestionRepository } from '../../../src/data/questions/question-repository';
import { getLearningObjectRepository, getQuestionRepository, getStudentRepository } from '../../../src/data/repositories';
import { getQuestionRepository, getStudentRepository } from '../../../src/data/repositories';
import { StudentRepository } from '../../../src/data/users/student-repository';
import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository';
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier';
describe('QuestionRepository', () => {
let questionRepository: QuestionRepository;
let studentRepository: StudentRepository;
let learningObjectRepository: LearningObjectRepository;
beforeAll(async () => {
await setupTestApp();
questionRepository = getQuestionRepository();
studentRepository = getStudentRepository();
learningObjectRepository = getLearningObjectRepository();
});
it('should return all questions part of the given learning object', async () => {

View file

@ -78,7 +78,7 @@ describe('DatabaseLearningObjectProvider', () => {
});
it('should throw an error if queried with a path identifier for which there is no learning path', async () => {
await expect(
(async () => {
(async (): Promise<void> => {
await databaseLearningObjectProvider.getLearningObjectIdsFromPath({
hruid: 'non_existing_hruid',
language: Language.Dutch,
@ -97,7 +97,7 @@ describe('DatabaseLearningObjectProvider', () => {
});
it('should throw an error if queried with a path identifier for which there is no learning path', async () => {
await expect(
(async () => {
(async (): Promise<void> => {
await databaseLearningObjectProvider.getLearningObjectsFromPath({
hruid: 'non_existing_hruid',
language: Language.Dutch,

View file

@ -5,7 +5,7 @@ import { LearningObject } from '../../../src/entities/content/learning-object.en
import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories';
import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example';
import learningObjectService from '../../../src/services/learning-objects/learning-object-service';
import { EnvVars, getEnvVar } from '../../../src/util/envvars';
import { envVars, getEnvVar } from '../../../src/util/envVars';
import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import learningPathExample from '../../test-assets/learning-paths/pn-werking-example';
import { LearningObjectIdentifier, LearningPathIdentifier } from 'dwengo-1-common/src/interfaces/learning-content';
@ -79,7 +79,7 @@ describe('LearningObjectService', () => {
expect(result).not.toBeNull();
const responseFromDwengoApi = await fetch(
getEnvVar(EnvVars.LearningContentRepoApiBaseUrl) +
getEnvVar(envVars.LearningContentRepoApiBaseUrl) +
`/learningObject/getRaw?hruid=${DWENGO_TEST_LEARNING_OBJECT_ID.hruid}&language=${DWENGO_TEST_LEARNING_OBJECT_ID.language}&version=${DWENGO_TEST_LEARNING_OBJECT_ID.version}`
);
const responseHtml = await responseFromDwengoApi.text();

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