refactor(backend): Types

This commit is contained in:
Tibo De Peuter 2025-03-23 11:14:32 +01:00
parent 6ad7fbf208
commit 25f9eb2af2
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
31 changed files with 92 additions and 86 deletions

View file

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

View file

@ -13,7 +13,7 @@ function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIde
throw new BadRequestException('HRUID is required.');
}
return {
hruid: req.params.hruid as string,
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

@ -85,7 +85,7 @@ export async function deleteTeacherHandler(req: Request, res: Response): Promise
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
try {
const username = req.params.username as string;
const username = req.params.username;
const full = req.query.full === 'true';
if (!username) {
@ -104,7 +104,7 @@ export async function getTeacherClassHandler(req: Request, res: Response): Promi
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
try {
const username = req.params.username as string;
const username = req.params.username;
const full = req.query.full === 'true';
if (!username) {
@ -123,7 +123,7 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
try {
const username = req.params.username as string;
const username = req.params.username;
const full = req.query.full === 'true';
if (!username) {

View file

@ -24,7 +24,7 @@ export async function getAllUsersHandler<T extends User>(req: Request, res: Resp
export async function getUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
try {
const username = req.params.username as string;
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });

View file

@ -58,7 +58,7 @@ export interface EducationalGoal {
export interface ReturnValue {
callback_url: string;
callback_schema: Record<string, any>;
callback_schema: Record<string, unknown>;
}
export interface LearningObjectMetadata {

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,6 @@
import { LearningObjectProvider } from './learning-object-provider.js';
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js';
import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js';
import { Language } from '../../entities/content/language.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { getUrlStringForLearningObject } from '../../util/links.js';
import processingService from './processing/processing-service.js';
@ -44,7 +43,7 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
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,7 +64,7 @@ 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;
}

View file

@ -8,6 +8,7 @@ import { DwengoContentType } from '../content-type.js';
import dwengoMarkedRenderer from './dwengo-marked-renderer.js';
import { StringProcessor } from '../string-processor.js';
import { ProcessingError } from '../processing-error.js';
import { YAMLException } from 'js-yaml';
class MarkdownProcessor extends StringProcessor {
constructor() {
@ -19,8 +20,12 @@ class MarkdownProcessor extends StringProcessor {
marked.use({ renderer: dwengoMarkedRenderer });
const html = marked(mdText, { async: false });
return this.replaceLinks(html); // Replace html image links path
} catch (e: any) {
throw new ProcessingError(e.message);
} catch (e: unknown) {
if (e instanceof YAMLException) {
throw new ProcessingError(e.message);
}
throw new ProcessingError('Unknown error while processing markdown: ' + e);
}
}

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

@ -25,7 +25,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma
version: node.version,
language: node.language,
})
.then((learningObject) => <[LearningPathNode, FilteredLearningObject | null]>[node, learningObject])
.then((learningObject) => ([node, learningObject] as [LearningPathNode, FilteredLearningObject | null]))
)
)
);

View file

@ -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 '../interfaces/learning-content.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

@ -5,7 +5,7 @@ const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_';
const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_';
const CORS_PREFIX = PREFIX + 'CORS_';
type EnvVar = { key: string; required?: boolean; defaultValue?: any };
interface EnvVar { key: string; required?: boolean; defaultValue?: number | string | boolean }
export const envVars: { [key: string]: EnvVar } = {
Port: { key: PREFIX + 'PORT', defaultValue: 3000 },
@ -44,7 +44,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

@ -1,8 +1,8 @@
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import { Attachment } from '../../../src/entities/content/attachment.entity';
type LearningObjectExample = {
interface LearningObjectExample {
createLearningObject: () => LearningObject;
createAttachment: { [key: string]: (owner: LearningObject) => Attachment };
getHTMLRendering: () => string;
};
}

View file

@ -1,3 +1,3 @@
type LearningPathExample = {
interface LearningPathExample {
createLearningPath: () => LearningPath;
};
}

View file

@ -6,12 +6,12 @@ import { createLearningPathNode, createLearningPathTransition } from './learning
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import { envVars, getEnvVar } from '../../../src/util/envVars';
export type ConditionTestLearningPathAndLearningObjects = {
export interface ConditionTestLearningPathAndLearningObjects {
branchingObject: LearningObject;
extraExerciseObject: LearningObject;
finalObject: LearningObject;
learningPath: LearningPath;
};
}
export function createConditionTestLearningPathAndLearningObjects(): ConditionTestLearningPathAndLearningObjects {
const learningPath = new LearningPath();

View file

@ -25,7 +25,7 @@ export function expectToBeCorrectEntity<T extends object>(actual: { entity: T; n
expected.entity[property] !== undefined && // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants.
typeof expected.entity[property] !== 'function' // Functions obviously are not persisted via the database
) {
if (!actual.entity.hasOwnProperty(property)) {
if (!Object.prototype.hasOwnProperty.call(actual.entity, property)) {
throw new AssertionError({
message: `${expected.name} has defined property ${property}, but ${actual.name} is missing it.`,
});

View file

@ -1,9 +1,9 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
import { Class } from '../../../src/entities/classes/class.entity';
import { Language } from '../../../src/entities/content/language';
export function makeTestAssignemnts(em: EntityManager<IDatabaseDriver<Connection>>, classes: Array<Class>): Array<Assignment> {
export function makeTestAssignemnts(em: EntityManager, classes: Array<Class>): Array<Assignment> {
const assignment01 = em.create(Assignment, {
within: classes[0],
id: 1,

View file

@ -1,13 +1,9 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { Group } from '../../../src/entities/assignments/group.entity';
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
import { Student } from '../../../src/entities/users/student.entity';
export function makeTestGroups(
em: EntityManager<IDatabaseDriver<Connection>>,
students: Array<Student>,
assignments: Array<Assignment>
): Array<Group> {
export function makeTestGroups(em: EntityManager, students: Array<Student>, assignments: Array<Assignment>): Array<Group> {
const group01 = em.create(Group, {
assignment: assignments[0],
groupNumber: 1,

View file

@ -1,14 +1,10 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { Submission } from '../../../src/entities/assignments/submission.entity';
import { Language } from '../../../src/entities/content/language';
import { Student } from '../../../src/entities/users/student.entity';
import { Group } from '../../../src/entities/assignments/group.entity';
export function makeTestSubmissions(
em: EntityManager<IDatabaseDriver<Connection>>,
students: Array<Student>,
groups: Array<Group>
): Array<Submission> {
export function makeTestSubmissions(em: EntityManager, students: Array<Student>, groups: Array<Group>): Array<Submission> {
const submission01 = em.create(Submission, {
learningObjectHruid: 'id03',
learningObjectLanguage: Language.English,

View file

@ -1,13 +1,9 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { ClassJoinRequest, ClassJoinRequestStatus } from '../../../src/entities/classes/class-join-request.entity';
import { Student } from '../../../src/entities/users/student.entity';
import { Class } from '../../../src/entities/classes/class.entity';
export function makeTestClassJoinRequests(
em: EntityManager<IDatabaseDriver<Connection>>,
students: Array<Student>,
classes: Array<Class>
): Array<ClassJoinRequest> {
export function makeTestClassJoinRequests(em: EntityManager, students: Array<Student>, classes: Array<Class>): Array<ClassJoinRequest> {
const classJoinRequest01 = em.create(ClassJoinRequest, {
requester: students[4],
class: classes[1],

View file

@ -1,9 +1,9 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { Class } from '../../../src/entities/classes/class.entity';
import { Student } from '../../../src/entities/users/student.entity';
import { Teacher } from '../../../src/entities/users/teacher.entity';
export function makeTestClasses(em: EntityManager<IDatabaseDriver<Connection>>, students: Array<Student>, teachers: Array<Teacher>): Array<Class> {
export function makeTestClasses(em: EntityManager, students: Array<Student>, teachers: Array<Teacher>): Array<Class> {
const studentsClass01 = students.slice(0, 7);
const teacherClass01: Array<Teacher> = teachers.slice(0, 1);

View file

@ -1,13 +1,9 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { TeacherInvitation } from '../../../src/entities/classes/teacher-invitation.entity';
import { Teacher } from '../../../src/entities/users/teacher.entity';
import { Class } from '../../../src/entities/classes/class.entity';
export function makeTestTeacherInvitations(
em: EntityManager<IDatabaseDriver<Connection>>,
teachers: Array<Teacher>,
classes: Array<Class>
): Array<TeacherInvitation> {
export function makeTestTeacherInvitations(em: EntityManager, teachers: Array<Teacher>, classes: Array<Class>): Array<TeacherInvitation> {
const teacherInvitation01 = em.create(TeacherInvitation, {
sender: teachers[1],
receiver: teachers[0],

View file

@ -1,8 +1,8 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { Attachment } from '../../../src/entities/content/attachment.entity';
import { LearningObject } from '../../../src/entities/content/learning-object.entity';
export function makeTestAttachments(em: EntityManager<IDatabaseDriver<Connection>>, learningObjects: Array<LearningObject>): Array<Attachment> {
export function makeTestAttachments(em: EntityManager, learningObjects: Array<LearningObject>): Array<Attachment> {
const attachment01 = em.create(Attachment, {
learningObject: learningObjects[1],
name: 'attachment01',

View file

@ -1,9 +1,9 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { LearningObject, ReturnValue } from '../../../src/entities/content/learning-object.entity';
import { Language } from '../../../src/entities/content/language';
import { DwengoContentType } from '../../../src/services/learning-objects/processing/content-type';
export function makeTestLearningObjects(em: EntityManager<IDatabaseDriver<Connection>>): Array<LearningObject> {
export function makeTestLearningObjects(em: EntityManager): Array<LearningObject> {
const returnValue: ReturnValue = new ReturnValue();
returnValue.callbackSchema = '';
returnValue.callbackUrl = '';

View file

@ -1,10 +1,10 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import { Language } from '../../../src/entities/content/language';
import { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity';
import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity';
export function makeTestLearningPaths(em: EntityManager<IDatabaseDriver<Connection>>): Array<LearningPath> {
export function makeTestLearningPaths(em: EntityManager): Array<LearningPath> {
const learningPathNode01: LearningPathNode = new LearningPathNode();
const learningPathNode02: LearningPathNode = new LearningPathNode();
const learningPathNode03: LearningPathNode = new LearningPathNode();

View file

@ -1,9 +1,9 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { Answer } from '../../../src/entities/questions/answer.entity';
import { Teacher } from '../../../src/entities/users/teacher.entity';
import { Question } from '../../../src/entities/questions/question.entity';
export function makeTestAnswers(em: EntityManager<IDatabaseDriver<Connection>>, teachers: Array<Teacher>, questions: Array<Question>): Array<Answer> {
export function makeTestAnswers(em: EntityManager, teachers: Array<Teacher>, questions: Array<Question>): Array<Answer> {
const answer01 = em.create(Answer, {
author: teachers[0],
toQuestion: questions[1],

View file

@ -1,9 +1,9 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { Question } from '../../../src/entities/questions/question.entity';
import { Language } from '../../../src/entities/content/language';
import { Student } from '../../../src/entities/users/student.entity';
export function makeTestQuestions(em: EntityManager<IDatabaseDriver<Connection>>, students: Array<Student>): Array<Question> {
export function makeTestQuestions(em: EntityManager, students: Array<Student>): Array<Question> {
const question01 = em.create(Question, {
learningObjectLanguage: Language.English,
learningObjectVersion: 1,

View file

@ -1,7 +1,7 @@
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
import { Student } from '../../../src/entities/users/student.entity';
export function makeTestStudents(em: EntityManager<IDatabaseDriver<Connection>>): Array<Student> {
export function makeTestStudents(em: EntityManager): Array<Student> {
const student01 = em.create(Student, {
username: 'Noordkaap',
firstName: 'Stijn',

View file

@ -1,7 +1,7 @@
import { Teacher } from '../../../src/entities/users/teacher.entity';
import { Connection, EntityManager, IDatabaseDriver } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/core';
export function makeTestTeachers(em: EntityManager<IDatabaseDriver<Connection>>): Array<Teacher> {
export function makeTestTeachers(em: EntityManager): Array<Teacher> {
const teacher01 = em.create(Teacher, {
username: 'FooFighters',
firstName: 'Dave',

View file

@ -34,6 +34,10 @@ export default [
rules: {
'consistent-return': 'off',
'@typescript-eslint/consistent-return': 'off',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/consistent-type-exports': 'off',
'@typescript-eslint/consistent-type-imports': 'off',
'@typescript-eslint/explicit-function-return-type': 'warn',
@ -58,12 +62,24 @@ export default [
}
],
'no-dupe-class-members': 'off',
'@typescript-eslint/no-dupe-class-members': 'off',
'@typescript-eslint/no-duplicate-enum-values': 'error',
'no-duplicate-imports': 'off',
'@typescript-eslint/no-duplicate-type-constituents': 'off',
// 'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'no-loop-func': 'off',
'@typescript-eslint/no-loop-func': 'error',
'@typescript-eslint/no-type-alias': 'off',
'@typescript-eslint/no-unnecessary-type-arguments': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unnecessary-type-constraint': 'error',
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
'@typescript-eslint/no-unsafe-function-type': 'error',
'no-unused-expressions': 'off',
@ -89,7 +105,6 @@ export default [
'no-await-in-loop': 'warn',
'no-constructor-return': 'error',
'no-duplicate-imports': 'error',
'no-inner-declarations': 'error',
'no-self-compare': 'error',
'no-template-curly-in-string': 'error',
@ -100,7 +115,6 @@ export default [
'arrow-body-style': ['warn', 'as-needed'],
'block-scoped-var': 'warn',
'capitalized-comments': 'warn',
'consistent-return': 'warn',
'consistent-this': 'error',
curly: 'error',
'default-case': 'error',