From bd75ab8af9f5ec5bd40f32c0161805b20cbe08ab Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 6 Apr 2025 09:45:01 +0200 Subject: [PATCH 01/14] fix: question service + refactor loID --- backend/src/controllers/learning-objects.ts | 4 +- .../src/data/questions/question-repository.ts | 9 +++ backend/src/interfaces/question.ts | 14 +++- .../learning-objects/attachment-service.ts | 4 +- .../database-learning-object-provider.ts | 8 +- .../dwengo-api-learning-object-provider.ts | 8 +- .../learning-object-provider.ts | 6 +- .../learning-object-service.ts | 8 +- .../markdown/dwengo-marked-renderer.ts | 4 +- .../processing/processing-service.ts | 4 +- backend/src/services/questions.ts | 74 ++++++++----------- backend/src/util/links.ts | 6 +- .../learning-object-service.test.ts | 4 +- common/src/interfaces/learning-content.ts | 2 +- common/src/interfaces/question.ts | 8 +- common/src/interfaces/submission.ts | 4 +- 16 files changed, 86 insertions(+), 81 deletions(-) diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index a2510631..83aa33f9 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -6,9 +6,9 @@ import attachmentService from '../services/learning-objects/attachment-service.j import { BadRequestException } from '../exceptions/bad-request-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; import { envVars, getEnvVar } from '../util/envVars.js'; -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; -function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { +function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { if (!req.params.hruid) { throw new BadRequestException('HRUID is required.'); } diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 2d165abc..90c144d8 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -61,4 +61,13 @@ export class QuestionRepository extends DwengoEntityRepository { orderBy: { timestamp: 'DESC' }, // New to old }); } + + public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number){ + return this.findOne({ + learningObjectHruid: loId.hruid, + learningObjectLanguage: loId.language, + learningObjectVersion: loId.version, + sequenceNumber + }); + } } diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 48d64f11..9005654f 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -1,9 +1,11 @@ import { Question } from '../entities/questions/question.entity.js'; import { mapToStudentDTO } from './student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; -import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { + +function getLearningObjectIdentifier(question: Question): LearningObjectIdentifierDTO { return { hruid: question.learningObjectHruid, language: question.learningObjectLanguage, @@ -11,6 +13,14 @@ function getLearningObjectIdentifier(question: Question): LearningObjectIdentifi }; } +export function mapToLearningObjectID(loID: LearningObjectIdentifierDTO): LearningObjectIdentifier { + return { + hruid: loID.hruid, + language: loID.language, + version: loID.version ?? 1 + } +} + /** * Convert a Question entity to a DTO format. */ diff --git a/backend/src/services/learning-objects/attachment-service.ts b/backend/src/services/learning-objects/attachment-service.ts index 46fc5e03..2a6298c1 100644 --- a/backend/src/services/learning-objects/attachment-service.ts +++ b/backend/src/services/learning-objects/attachment-service.ts @@ -1,10 +1,10 @@ import { getAttachmentRepository } from '../../data/repositories.js'; import { Attachment } from '../../entities/content/attachment.entity.js'; -import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; const attachmentService = { - async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise { + async getAttachment(learningObjectId: LearningObjectIdentifierDTO, attachmentName: string): Promise { const attachmentRepo = getAttachmentRepository(); if (learningObjectId.version) { diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts index 361153f5..fa278bba 100644 --- a/backend/src/services/learning-objects/database-learning-object-provider.ts +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -6,7 +6,7 @@ import processingService from './processing/processing-service.js'; import { NotFoundError } from '@mikro-orm/core'; import learningObjectService from './learning-object-service.js'; import { getLogger, Logger } from '../../logging/initalize.js'; -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; const logger: Logger = getLogger(); @@ -40,7 +40,7 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL }; } -async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise { +async function findLearningObjectEntityById(id: LearningObjectIdentifierDTO): Promise { const learningObjectRepo = getLearningObjectRepository(); return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); @@ -53,7 +53,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { /** * Fetches a single learning object by its HRUID */ - async getLearningObjectById(id: LearningObjectIdentifier): Promise { + async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise { const learningObject = await findLearningObjectEntityById(id); return convertLearningObject(learningObject); }, @@ -61,7 +61,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { /** * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ - async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise { const learningObjectRepo = getLearningObjectRepository(); const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); diff --git a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index d67b69ae..e9898f62 100644 --- a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts @@ -5,7 +5,7 @@ import { LearningObjectProvider } from './learning-object-provider.js'; import { getLogger, Logger } from '../../logging/initalize.js'; import { FilteredLearningObject, - LearningObjectIdentifier, + LearningObjectIdentifierDTO, LearningObjectMetadata, LearningObjectNode, LearningPathIdentifier, @@ -67,7 +67,7 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full const objects = await Promise.all( nodes.map(async (node) => { - const learningObjectId: LearningObjectIdentifier = { + const learningObjectId: LearningObjectIdentifierDTO = { hruid: node.learningobject_hruid, language: learningPathId.language, }; @@ -85,7 +85,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { /** * Fetches a single learning object by its HRUID */ - async getLearningObjectById(id: LearningObjectIdentifier): Promise { + async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise { const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; const metadata = await fetchWithLogging( metadataUrl, @@ -121,7 +121,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { * Obtain a HTML-rendering of the learning object with the given identifier (as a string). For learning objects * from the Dwengo API, this means passing through the HTML rendering from there. */ - async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise { const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; const html = await fetchWithLogging(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { params: { ...id }, diff --git a/backend/src/services/learning-objects/learning-object-provider.ts b/backend/src/services/learning-objects/learning-object-provider.ts index a0fcb552..14848bc0 100644 --- a/backend/src/services/learning-objects/learning-object-provider.ts +++ b/backend/src/services/learning-objects/learning-object-provider.ts @@ -1,10 +1,10 @@ -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; export interface LearningObjectProvider { /** * Fetches a single learning object by its HRUID */ - getLearningObjectById(id: LearningObjectIdentifier): Promise; + getLearningObjectById(id: LearningObjectIdentifierDTO): Promise; /** * Fetch full learning object data (metadata) @@ -19,5 +19,5 @@ export interface LearningObjectProvider { /** * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ - getLearningObjectHTML(id: LearningObjectIdentifier): Promise; + getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise; } diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 5a06f0f2..7b4f47fc 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -2,9 +2,9 @@ import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provid import { LearningObjectProvider } from './learning-object-provider.js'; import { envVars, getEnvVar } from '../../util/envVars.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js'; -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; -function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { +function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { return databaseLearningObjectProvider; } @@ -18,7 +18,7 @@ const learningObjectService = { /** * Fetches a single learning object by its HRUID */ - async getLearningObjectById(id: LearningObjectIdentifier): Promise { + async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise { return getProvider(id).getLearningObjectById(id); }, @@ -39,7 +39,7 @@ const learningObjectService = { /** * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ - async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise { return getProvider(id).getLearningObjectHTML(id); }, }; diff --git a/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts b/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts index 87ba13b7..e01d8ed6 100644 --- a/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts +++ b/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts @@ -12,7 +12,7 @@ import Image = marked.Tokens.Image; import Heading = marked.Tokens.Heading; import Link = marked.Tokens.Link; import RendererObject = marked.RendererObject; -import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; import { Language } from '@dwengo-1/common/util/language'; const prefixes = { @@ -25,7 +25,7 @@ const prefixes = { blockly: '@blockly', }; -function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier { +function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifierDTO { const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split('/'); return { hruid, diff --git a/backend/src/services/learning-objects/processing/processing-service.ts b/backend/src/services/learning-objects/processing/processing-service.ts index e9147e31..61821d80 100644 --- a/backend/src/services/learning-objects/processing/processing-service.ts +++ b/backend/src/services/learning-objects/processing/processing-service.ts @@ -14,7 +14,7 @@ import { LearningObject } from '../../../entities/content/learning-object.entity import Processor from './processor.js'; import { DwengoContentType } from './content-type.js'; import { replaceAsync } from '../../../util/async.js'; -import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; import { Language } from '@dwengo-1/common/util/language'; const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = //g; @@ -50,7 +50,7 @@ class ProcessingService { */ async render( learningObject: LearningObject, - fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifier) => Promise + fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifierDTO) => Promise ): Promise { const html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject); if (fetchEmbeddedLearningObjects) { diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 319061c5..d983c3be 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -1,22 +1,21 @@ import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; -import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; +import {mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId} from '../interfaces/question.js'; import { Question } from '../entities/questions/question.entity.js'; import { Answer } from '../entities/questions/answer.entity.js'; import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; import { QuestionRepository } from '../data/questions/question-repository.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import { mapToStudent } from '../interfaces/student.js'; +import {mapToStudent, mapToStudentDTO} from '../interfaces/student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; +import {NotFoundException} from "../exceptions/not-found-exception"; +import {fetchStudent} from "./students"; +import {Student} from "../entities/users/student.entity"; export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { const questionRepository: QuestionRepository = getQuestionRepository(); const questions = await questionRepository.findAllQuestionsAboutLearningObject(id); - if (!questions) { - return []; - } - if (full) { return questions.map(mapToQuestionDTO); } @@ -24,24 +23,22 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea return questions.map(mapToQuestionDTOId); } -async function fetchQuestion(questionId: QuestionId): Promise { +async function fetchQuestion(questionId: QuestionId): Promise { const questionRepository = getQuestionRepository(); + const question = await questionRepository.findByLearningObjectAndSequenceNumber( + mapToLearningObjectID(questionId.learningObjectIdentifier), + questionId.sequenceNumber + ); - return await questionRepository.findOne({ - learningObjectHruid: questionId.learningObjectIdentifier.hruid, - learningObjectLanguage: questionId.learningObjectIdentifier.language, - learningObjectVersion: questionId.learningObjectIdentifier.version, - sequenceNumber: questionId.sequenceNumber, - }); -} - -export async function getQuestion(questionId: QuestionId): Promise { - const question = await fetchQuestion(questionId); - - if (!question) { - return null; + if (!question){ + throw new NotFoundException('Question with loID and sequence number not found'); } + return question; +} + +export async function getQuestion(questionId: QuestionId): Promise { + const question = await fetchQuestion(questionId); return mapToQuestionDTO(question); } @@ -49,16 +46,8 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean const answerRepository = getAnswerRepository(); const question = await fetchQuestion(questionId); - if (!question) { - return []; - } - const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question); - if (!answers) { - return []; - } - if (full) { return answers.map(mapToAnswerDTO); } @@ -68,24 +57,21 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean export async function createQuestion(questionDTO: QuestionDTO): Promise { const questionRepository = getQuestionRepository(); - - const author = mapToStudent(questionDTO.author); - - const loId: LearningObjectIdentifier = { - ...questionDTO.learningObjectIdentifier, - version: questionDTO.learningObjectIdentifier.version ?? 1, - }; - - try { - await questionRepository.createQuestion({ - loId, - author, - content: questionDTO.content, - }); - } catch (_) { - return null; + let author: Student; + if (typeof questionDTO.author === "string" ){ + author = await fetchStudent(questionDTO.author); + } else { + author = mapToStudent(questionDTO.author) } + + await questionRepository.createQuestion({ + loId: mapToLearningObjectID(questionDTO.learningObjectIdentifier), + author, + content: questionDTO.content, + }); + + return questionDTO; } diff --git a/backend/src/util/links.ts b/backend/src/util/links.ts index 9ede7d12..106613a4 100644 --- a/backend/src/util/links.ts +++ b/backend/src/util/links.ts @@ -1,4 +1,4 @@ -import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; export function isValidHttpUrl(url: string): boolean { try { @@ -9,7 +9,7 @@ export function isValidHttpUrl(url: string): boolean { } } -export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifier): string { +export function getUrlStringForLearningObject(learningObjectId: LearningObjectIdentifierDTO): string { let url = `/learningObject/${learningObjectId.hruid}/html?language=${learningObjectId.language}`; if (learningObjectId.version) { url += `&version=${learningObjectId.version}`; @@ -17,7 +17,7 @@ export function getUrlStringForLearningObject(learningObjectId: LearningObjectId return url; } -export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifier): string { +export function getUrlStringForLearningObjectHTML(learningObjectIdentifier: LearningObjectIdentifierDTO): string { let url = `/learningObject/${learningObjectIdentifier.hruid}/html?language=${learningObjectIdentifier.language}`; if (learningObjectIdentifier.version) { url += `&version=${learningObjectIdentifier.version}`; diff --git a/backend/tests/services/learning-objects/learning-object-service.test.ts b/backend/tests/services/learning-objects/learning-object-service.test.ts index a0fea849..3ea4143d 100644 --- a/backend/tests/services/learning-objects/learning-object-service.test.ts +++ b/backend/tests/services/learning-objects/learning-object-service.test.ts @@ -7,11 +7,11 @@ import learningObjectService from '../../../src/services/learning-objects/learni 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/interfaces/learning-content'; +import { LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { Language } from '@dwengo-1/common/util/language'; const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks'; -const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifier = { +const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifierDTO = { hruid: 'pn_werkingnotebooks', language: Language.Dutch, version: 3, diff --git a/common/src/interfaces/learning-content.ts b/common/src/interfaces/learning-content.ts index 02e49648..435e7001 100644 --- a/common/src/interfaces/learning-content.ts +++ b/common/src/interfaces/learning-content.ts @@ -11,7 +11,7 @@ export interface Transition { }; } -export interface LearningObjectIdentifier { +export interface LearningObjectIdentifierDTO { hruid: string; language: Language; version?: number; diff --git a/common/src/interfaces/question.ts b/common/src/interfaces/question.ts index b80ff0af..3f24480a 100644 --- a/common/src/interfaces/question.ts +++ b/common/src/interfaces/question.ts @@ -1,15 +1,15 @@ -import { LearningObjectIdentifier } from './learning-content'; +import { LearningObjectIdentifierDTO } from './learning-content'; import { StudentDTO } from './student'; export interface QuestionDTO { - learningObjectIdentifier: LearningObjectIdentifier; + learningObjectIdentifier: LearningObjectIdentifierDTO; sequenceNumber?: number; - author: StudentDTO; + author: StudentDTO | string; timestamp?: string; content: string; } export interface QuestionId { - learningObjectIdentifier: LearningObjectIdentifier; + learningObjectIdentifier: LearningObjectIdentifierDTO; sequenceNumber: number; } diff --git a/common/src/interfaces/submission.ts b/common/src/interfaces/submission.ts index 6b250616..9101df79 100644 --- a/common/src/interfaces/submission.ts +++ b/common/src/interfaces/submission.ts @@ -1,10 +1,10 @@ import { GroupDTO } from './group'; -import { LearningObjectIdentifier } from './learning-content'; +import { LearningObjectIdentifierDTO } from './learning-content'; import { StudentDTO } from './student'; import { Language } from '../util/language'; export interface SubmissionDTO { - learningObjectIdentifier: LearningObjectIdentifier; + learningObjectIdentifier: LearningObjectIdentifierDTO; submissionNumber?: number; submitter: StudentDTO; From 6a1adb0ee3736f64d22127d6d7b13853d36d5f83 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sun, 6 Apr 2025 18:30:43 +0200 Subject: [PATCH 02/14] fix: question controller --- backend/src/config.ts | 1 + backend/src/controllers/questions.ts | 112 +++++++++++---------------- backend/src/services/questions.ts | 22 +----- common/src/interfaces/question.ts | 2 +- 4 files changed, 49 insertions(+), 88 deletions(-) diff --git a/backend/src/config.ts b/backend/src/config.ts index 9b4702b5..9b209ada 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -5,3 +5,4 @@ export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl); export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage); export const FALLBACK_SEQ_NUM = 1; +export const FALLBACK_VERSION_NUM = 1; diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index b5b764ac..a4a6a76d 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -1,34 +1,20 @@ import { Request, Response } from 'express'; import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; -import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; +import {FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM} from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; +import {requireFields} from "./error-helper"; -function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { - const { hruid, version } = req.params; - const lang = req.query.lang; - - if (!hruid || !version) { - res.status(400).json({ error: 'Missing required parameters.' }); - return null; - } - +function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier { return { hruid, - language: (lang as Language) || FALLBACK_LANG, - version: Number(version), + language: (lang || FALLBACK_LANG) as Language, + version: Number(version) || FALLBACK_VERSION_NUM, }; } -function getQuestionId(req: Request, res: Response): QuestionId | null { - const seq = req.params.seq; - const learningObjectIdentifier = getObjectId(req, res); - - if (!learningObjectIdentifier) { - return null; - } - +function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId { return { learningObjectIdentifier, sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, @@ -36,84 +22,76 @@ function getQuestionId(req: Request, res: Response): QuestionId | null { } export async function getAllQuestionsHandler(req: Request, res: Response): Promise { - const objectId = getObjectId(req, res); + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; const full = req.query.full === 'true'; + requireFields({ hruid }) - if (!objectId) { - return; - } + const learningObjectId = getLearningObjectId(hruid, version, language); - const questions = await getAllQuestions(objectId, full); + const questions = await getAllQuestions(learningObjectId, full); - if (!questions) { - res.status(404).json({ error: `Questions not found.` }); - } else { - res.json({ questions: questions }); - } + res.json({ questions }); } export async function getQuestionHandler(req: Request, res: Response): Promise { - const questionId = getQuestionId(req, res); + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }) - if (!questionId) { - return; - } + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); const question = await getQuestion(questionId); - if (!question) { - res.status(404).json({ error: `Question not found.` }); - } else { - res.json(question); - } + res.json({ question }); + } export async function getQuestionAnswersHandler(req: Request, res: Response): Promise { - const questionId = getQuestionId(req, res); + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; const full = req.query.full === 'true'; + requireFields({ hruid }) - if (!questionId) { - return; - } + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); const answers = await getAnswersByQuestion(questionId, full); - if (!answers) { - res.status(404).json({ error: `Questions not found` }); - } else { - res.json({ answers: answers }); - } + res.json({ answers }); } export async function createQuestionHandler(req: Request, res: Response): Promise { - const questionDTO = req.body as QuestionDTO; + const learningObjectIdentifier = req.body.learningObjectIdentifier; + const author = req.body.author; + const content = req.body.content; + requireFields({ learningObjectIdentifier, author, content }); - if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) { - res.status(400).json({ error: 'Missing required fields: identifier and content' }); - return; - } + const questionDTO = req.body as QuestionDTO; const question = await createQuestion(questionDTO); - if (!question) { - res.status(400).json({ error: 'Could not create question' }); - } else { - res.json(question); - } + res.json({ question }); + } export async function deleteQuestionHandler(req: Request, res: Response): Promise { - const questionId = getQuestionId(req, res); + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }) - if (!questionId) { - return; - } + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); const question = await deleteQuestion(questionId); - if (!question) { - res.status(400).json({ error: 'Could not find nor delete question' }); - } else { - res.json(question); - } + res.json({ question }); } diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index d983c3be..be361085 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -5,12 +5,10 @@ import { Answer } from '../entities/questions/answer.entity.js'; import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; import { QuestionRepository } from '../data/questions/question-repository.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import {mapToStudent, mapToStudentDTO} from '../interfaces/student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; import {NotFoundException} from "../exceptions/not-found-exception"; import {fetchStudent} from "./students"; -import {Student} from "../entities/users/student.entity"; export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { const questionRepository: QuestionRepository = getQuestionRepository(); @@ -57,13 +55,7 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean export async function createQuestion(questionDTO: QuestionDTO): Promise { const questionRepository = getQuestionRepository(); - let author: Student; - if (typeof questionDTO.author === "string" ){ - author = await fetchStudent(questionDTO.author); - } else { - author = mapToStudent(questionDTO.author) - } - + const author = await fetchStudent(questionDTO.author); await questionRepository.createQuestion({ loId: mapToLearningObjectID(questionDTO.learningObjectIdentifier), @@ -71,7 +63,6 @@ export async function createQuestion(questionDTO: QuestionDTO): Promise Date: Sun, 6 Apr 2025 23:37:54 +0200 Subject: [PATCH 03/14] feat: answer routes en put question --- backend/src/controllers/answers.ts | 101 ++++++++++++++++++ backend/src/controllers/questions.ts | 57 ++++++---- .../src/data/questions/answer-repository.ts | 10 ++ .../src/data/questions/question-repository.ts | 6 ++ backend/src/interfaces/answer.ts | 6 +- backend/src/interfaces/question.ts | 4 +- backend/src/routes/answers.ts | 22 ++++ backend/src/routes/questions.ts | 5 +- backend/src/services/answers.ts | 68 ++++++++++++ backend/src/services/questions.ts | 53 ++++----- common/src/interfaces/answer.ts | 9 +- common/src/interfaces/question.ts | 9 +- 12 files changed, 288 insertions(+), 62 deletions(-) create mode 100644 backend/src/controllers/answers.ts create mode 100644 backend/src/routes/answers.ts create mode 100644 backend/src/services/answers.ts diff --git a/backend/src/controllers/answers.ts b/backend/src/controllers/answers.ts new file mode 100644 index 00000000..1bb53019 --- /dev/null +++ b/backend/src/controllers/answers.ts @@ -0,0 +1,101 @@ +import {Request, Response} from "express"; +import {requireFields} from "./error-helper"; +import {getLearningObjectId, getQuestionId} from "./questions"; +import {createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer} from "../services/answers"; +import {FALLBACK_SEQ_NUM} from "../config"; +import {AnswerData} from "@dwengo-1/common/interfaces/answer"; + +export async function getAnswersHandler(req: Request, res: Response): Promise { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + const full = req.query.full === 'true'; + requireFields({ hruid }) + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const answers = await getAnswersByQuestion(questionId, full); + + res.json({ answers }); +} + +export async function getAnswerHandler(req: Request, res: Response): Promise{ + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + const seqAnswer = req.params.seqAnswer; + requireFields({ hruid }) + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; + const answer = await getAnswer(questionId, sequenceNumber); + + res.json({ answer }); +} + +export async function createAnswerHandler(req: Request, res: Response): Promise { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }) + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const author = req.body.author as string; + const content = req.body.content as string; + requireFields({ author, content }); + + const answerData = req.body as AnswerData; + + const answer = await createAnswer(questionId, answerData); + + res.json({ answer }); +} + +export async function deleteAnswerHandler(req: Request, res: Response): Promise { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + const seqAnswer = req.params.seqAnswer; + requireFields({ hruid }) + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; + const answer = await deleteAnswer(questionId, sequenceNumber); + + res.json({ answer }); +} + + + +export async function updateAnswerHandler(req: Request, res: Response): Promise { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + const seqAnswer = req.params.seqAnswer; + requireFields({ hruid }) + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const content = req.body.content as string; + requireFields({ content }); + + const answerData = req.body as AnswerData; + + const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; + const answer = await updateAnswer(questionId, sequenceNumber, answerData); + + res.json({ answer }); +} diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index a4a6a76d..e239d9ca 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -1,12 +1,17 @@ import { Request, Response } from 'express'; -import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; +import { + createQuestion, + deleteQuestion, + getAllQuestions, + getQuestion, updateQuestion +} from '../services/questions.js'; import {FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM} from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import {QuestionData, QuestionId} from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; import {requireFields} from "./error-helper"; -function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier { +export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier { return { hruid, language: (lang || FALLBACK_LANG) as Language, @@ -14,7 +19,7 @@ function getLearningObjectId(hruid: string, version: string, lang: string): Lear }; } -function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId { +export function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId { return { learningObjectIdentifier, sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, @@ -51,31 +56,21 @@ export async function getQuestionHandler(req: Request, res: Response): Promise { +export async function createQuestionHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; const version = req.params.version; const language = req.query.lang as string; - const seq = req.params.seq; - const full = req.query.full === 'true'; requireFields({ hruid }) - const learningObjectId = getLearningObjectId(hruid, version, language); - const questionId = getQuestionId(learningObjectId, seq); + const loId = getLearningObjectId(hruid, version, language); - const answers = await getAnswersByQuestion(questionId, full); + const author = req.body.author as string; + const content = req.body.content as string; + requireFields({ author, content }); - res.json({ answers }); -} + const questionData = req.body as QuestionData; -export async function createQuestionHandler(req: Request, res: Response): Promise { - const learningObjectIdentifier = req.body.learningObjectIdentifier; - const author = req.body.author; - const content = req.body.content; - requireFields({ learningObjectIdentifier, author, content }); - - const questionDTO = req.body as QuestionDTO; - - const question = await createQuestion(questionDTO); + const question = await createQuestion(loId, questionData); res.json({ question }); @@ -95,3 +90,23 @@ export async function deleteQuestionHandler(req: Request, res: Response): Promis res.json({ question }); } + +export async function updateQuestionHandler(req: Request, res: Response): Promise { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }) + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const content = req.body.content as string; + requireFields({ content }); + + const questionData = req.body as QuestionData; + + const question = await updateQuestion(questionId, questionData); + + res.json({ question }); +} diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index a50bfd28..94798248 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -19,10 +19,20 @@ export class AnswerRepository extends DwengoEntityRepository { orderBy: { sequenceNumber: 'ASC' }, }); } + public async findAnswer(question: Question, sequenceNumber: number) { + return this.findOne({ + toQuestion: question, sequenceNumber + }); + } public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { return this.deleteWhere({ toQuestion: question, sequenceNumber: sequenceNumber, }); } + public async updateContent(answer: Answer, newContent: string){ + answer.content = newContent; + await this.save(answer); + return answer; + } } diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 90c144d8..31a856a6 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -70,4 +70,10 @@ export class QuestionRepository extends DwengoEntityRepository { sequenceNumber }); } + + public async updateContent(question: Question, newContent: string): Promise { + question.content = newContent; + await this.save(question); + return question; + } } diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts index 1f0d0625..87464a0b 100644 --- a/backend/src/interfaces/answer.ts +++ b/backend/src/interfaces/answer.ts @@ -1,14 +1,14 @@ -import { mapToUserDTO } from './user.js'; -import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; +import {mapToQuestionDTO, mapToQuestionDTOId} from './question.js'; import { Answer } from '../entities/questions/answer.entity.js'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; +import {mapToTeacherDTO} from "./teacher"; /** * Convert a Question entity to a DTO format. */ export function mapToAnswerDTO(answer: Answer): AnswerDTO { return { - author: mapToUserDTO(answer.author), + author: mapToTeacherDTO(answer.author), toQuestion: mapToQuestionDTO(answer.toQuestion), sequenceNumber: answer.sequenceNumber!, timestamp: answer.timestamp.toISOString(), diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 9005654f..46f61825 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -1,6 +1,6 @@ import { Question } from '../entities/questions/question.entity.js'; -import { mapToStudentDTO } from './student.js'; -import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import { mapToStudentDTO} from './student.js'; +import { QuestionDTO, QuestionId} from '@dwengo-1/common/interfaces/question'; import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts new file mode 100644 index 00000000..edb27813 --- /dev/null +++ b/backend/src/routes/answers.ts @@ -0,0 +1,22 @@ +import express from "express"; +import { + createAnswerHandler, + deleteAnswerHandler, + getAnswerHandler, + getAnswersHandler, + updateAnswerHandler +} from "../controllers/answers"; + +const router = express.Router({ mergeParams: true }); + +router.get('/', getAnswersHandler); + +router.post('/', createAnswerHandler) + +router.get('/:seqAnswer', getAnswerHandler) + +router.delete('/:seqAnswer', deleteAnswerHandler); + +router.put('/:seqAnswer', updateAnswerHandler); + +export default router; diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index 31a71f3b..ba39435c 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -3,9 +3,10 @@ import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, - getQuestionAnswersHandler, getQuestionHandler, } from '../controllers/questions.js'; +import answerRoutes from './answers.js'; + const router = express.Router({ mergeParams: true }); // Query language @@ -20,6 +21,6 @@ router.delete('/:seq', deleteQuestionHandler); // Information about a question with id router.get('/:seq', getQuestionHandler); -router.get('/answers/:seq', getQuestionAnswersHandler); +router.use('/:seq/answer', answerRoutes); export default router; diff --git a/backend/src/services/answers.ts b/backend/src/services/answers.ts new file mode 100644 index 00000000..27b9cb13 --- /dev/null +++ b/backend/src/services/answers.ts @@ -0,0 +1,68 @@ +import {getAnswerRepository} from "../data/repositories"; +import {Answer} from "../entities/questions/answer.entity"; +import {mapToAnswerDTO, mapToAnswerDTOId} from "../interfaces/answer"; +import {fetchTeacher} from "./teachers"; +import {fetchQuestion} from "./questions"; +import {QuestionId} from "@dwengo-1/common/interfaces/question"; +import {AnswerData, AnswerDTO, AnswerId} from "@dwengo-1/common/interfaces/answer"; +import {NotFoundException} from "../exceptions/not-found-exception"; + +export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise { + const answerRepository = getAnswerRepository(); + const question = await fetchQuestion(questionId); + + const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question); + + if (full) { + return answers.map(mapToAnswerDTO); + } + + return answers.map(mapToAnswerDTOId); +} + +export async function createAnswer(questionId: QuestionId, answerData: AnswerData): Promise { + const answerRepository = getAnswerRepository(); + const toQuestion = await fetchQuestion(questionId); + const author = await fetchTeacher(answerData.author); + const content = answerData.content; + + const answer = await answerRepository.createAnswer({ + toQuestion, author, content + }); + return mapToAnswerDTO(answer); +} + +async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise { + const answerRepository = getAnswerRepository(); + const question = await fetchQuestion(questionId); + const answer = await answerRepository.findAnswer(question, sequenceNumber); + + if (!answer){ + throw new NotFoundException('Answer with questionID and sequence number not found'); + } + + return answer; +} + +export async function getAnswer(questionId: QuestionId, sequenceNumber: number): Promise { + const answer = await fetchAnswer(questionId, sequenceNumber); + return mapToAnswerDTO(answer); +} + +export async function deleteAnswer(questionId: QuestionId, sequenceNumber: number): Promise { + const answerRepository = getAnswerRepository(); + + const question = await fetchQuestion(questionId); + const answer = await fetchAnswer(questionId, sequenceNumber); + + await answerRepository.removeAnswerByQuestionAndSequenceNumber(question, sequenceNumber); + return mapToAnswerDTO(answer); +} + +export async function updateAnswer(questionId: QuestionId, sequenceNumber: number, answerData: AnswerData){ + const answerRepository = getAnswerRepository(); + const answer = await fetchAnswer(questionId, sequenceNumber); + + const newAnswer = await answerRepository.updateContent(answer, answerData.content); + return mapToAnswerDTO(newAnswer); +} diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index be361085..2e26c960 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -1,13 +1,11 @@ -import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; +import { getQuestionRepository } from '../data/repositories.js'; import {mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId} from '../interfaces/question.js'; import { Question } from '../entities/questions/question.entity.js'; -import { Answer } from '../entities/questions/answer.entity.js'; -import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; import { QuestionRepository } from '../data/questions/question-repository.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; -import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; +import {QuestionData, QuestionDTO, QuestionId} from '@dwengo-1/common/interfaces/question'; import {NotFoundException} from "../exceptions/not-found-exception"; +import {FALLBACK_VERSION_NUM} from "../config"; import {fetchStudent} from "./students"; export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { @@ -21,7 +19,7 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea return questions.map(mapToQuestionDTOId); } -async function fetchQuestion(questionId: QuestionId): Promise { +export async function fetchQuestion(questionId: QuestionId): Promise { const questionRepository = getQuestionRepository(); const question = await questionRepository.findByLearningObjectAndSequenceNumber( mapToLearningObjectID(questionId.learningObjectIdentifier), @@ -40,42 +38,37 @@ export async function getQuestion(questionId: QuestionId): Promise return mapToQuestionDTO(question); } -export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise { - const answerRepository = getAnswerRepository(); - const question = await fetchQuestion(questionId); - - const answers: Answer[] = await answerRepository.findAllAnswersToQuestion(question); - - if (full) { - return answers.map(mapToAnswerDTO); - } - - return answers.map(mapToAnswerDTOId); -} - -export async function createQuestion(questionDTO: QuestionDTO): Promise { +export async function createQuestion(loId: LearningObjectIdentifier, questionData: QuestionData): Promise { const questionRepository = getQuestionRepository(); - const author = await fetchStudent(questionDTO.author); + const author = await fetchStudent(questionData.author!); + const content = questionData.content; - await questionRepository.createQuestion({ - loId: mapToLearningObjectID(questionDTO.learningObjectIdentifier), - author, - content: questionDTO.content, + const question = await questionRepository.createQuestion({ + loId, author, content }); - return questionDTO; + return mapToQuestionDTO(question); } -export async function deleteQuestion(questionId: QuestionId): Promise { +export async function deleteQuestion(questionId: QuestionId): Promise { const questionRepository = getQuestionRepository(); - - const question = await fetchQuestion(questionId); + const question = await fetchQuestion(questionId); // throws error if not found const loId: LearningObjectIdentifier = { ...questionId.learningObjectIdentifier, - version: questionId.learningObjectIdentifier.version ?? 1, + version: questionId.learningObjectIdentifier.version ?? FALLBACK_VERSION_NUM, }; await questionRepository.removeQuestionByLearningObjectAndSequenceNumber(loId, questionId.sequenceNumber); return mapToQuestionDTO(question); } + +export async function updateQuestion(questionId: QuestionId, questionData: QuestionData): Promise { + const questionRepository = getQuestionRepository(); + const question = await fetchQuestion(questionId); + + const newQuestion = await questionRepository.updateContent(question, questionData.content); + return mapToQuestionDTO(newQuestion); +} + + diff --git a/common/src/interfaces/answer.ts b/common/src/interfaces/answer.ts index e9280f8a..efba9437 100644 --- a/common/src/interfaces/answer.ts +++ b/common/src/interfaces/answer.ts @@ -1,14 +1,19 @@ -import { UserDTO } from './user'; import { QuestionDTO, QuestionId } from './question'; +import {TeacherDTO} from "./teacher"; export interface AnswerDTO { - author: UserDTO; + author: TeacherDTO; toQuestion: QuestionDTO; sequenceNumber: number; timestamp: string; content: string; } +export interface AnswerData { + author: string; + content: string; +} + export interface AnswerId { author: string; toQuestion: QuestionId; diff --git a/common/src/interfaces/question.ts b/common/src/interfaces/question.ts index 88e9c7b8..9866fbef 100644 --- a/common/src/interfaces/question.ts +++ b/common/src/interfaces/question.ts @@ -4,8 +4,13 @@ import { StudentDTO } from './student'; export interface QuestionDTO { learningObjectIdentifier: LearningObjectIdentifierDTO; sequenceNumber?: number; - author: string; - timestamp?: string; + author: StudentDTO; + timestamp: string; + content: string; +} + +export interface QuestionData { + author?: string; content: string; } From 7ddde18d6c2b3cd8f84c7f43029b8d5dd76b64e6 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sun, 6 Apr 2025 21:41:37 +0000 Subject: [PATCH 04/14] style: fix linting issues met ESLint --- backend/src/services/questions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 2e26c960..21794ff5 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -52,7 +52,7 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat export async function deleteQuestion(questionId: QuestionId): Promise { const questionRepository = getQuestionRepository(); - const question = await fetchQuestion(questionId); // throws error if not found + const question = await fetchQuestion(questionId); // Throws error if not found const loId: LearningObjectIdentifier = { ...questionId.learningObjectIdentifier, From 9136ff031301b02bfd7ec4cfec31ed9f24001ef5 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sun, 6 Apr 2025 21:41:42 +0000 Subject: [PATCH 05/14] style: fix linting issues met Prettier --- backend/src/controllers/answers.ts | 26 +++++++++---------- backend/src/controllers/questions.ts | 25 +++++++----------- .../src/data/questions/answer-repository.ts | 5 ++-- .../src/data/questions/question-repository.ts | 4 +-- backend/src/interfaces/answer.ts | 4 +-- backend/src/interfaces/question.ts | 9 +++---- backend/src/routes/answers.ts | 14 +++------- backend/src/routes/questions.ts | 7 +---- backend/src/services/answers.ts | 24 +++++++++-------- backend/src/services/questions.ts | 18 ++++++------- common/src/interfaces/answer.ts | 2 +- 11 files changed, 60 insertions(+), 78 deletions(-) diff --git a/backend/src/controllers/answers.ts b/backend/src/controllers/answers.ts index 1bb53019..4167e746 100644 --- a/backend/src/controllers/answers.ts +++ b/backend/src/controllers/answers.ts @@ -1,9 +1,9 @@ -import {Request, Response} from "express"; -import {requireFields} from "./error-helper"; -import {getLearningObjectId, getQuestionId} from "./questions"; -import {createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer} from "../services/answers"; -import {FALLBACK_SEQ_NUM} from "../config"; -import {AnswerData} from "@dwengo-1/common/interfaces/answer"; +import { Request, Response } from 'express'; +import { requireFields } from './error-helper'; +import { getLearningObjectId, getQuestionId } from './questions'; +import { createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer } from '../services/answers'; +import { FALLBACK_SEQ_NUM } from '../config'; +import { AnswerData } from '@dwengo-1/common/interfaces/answer'; export async function getAnswersHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; @@ -11,7 +11,7 @@ export async function getAnswersHandler(req: Request, res: Response): Promise{ +export async function getAnswerHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; const version = req.params.version; const language = req.query.lang as string; const seq = req.params.seq; const seqAnswer = req.params.seqAnswer; - requireFields({ hruid }) + requireFields({ hruid }); const learningObjectId = getLearningObjectId(hruid, version, language); const questionId = getQuestionId(learningObjectId, seq); @@ -43,7 +43,7 @@ export async function createAnswerHandler(req: Request, res: Response): Promise< const version = req.params.version; const language = req.query.lang as string; const seq = req.params.seq; - requireFields({ hruid }) + requireFields({ hruid }); const learningObjectId = getLearningObjectId(hruid, version, language); const questionId = getQuestionId(learningObjectId, seq); @@ -65,7 +65,7 @@ export async function deleteAnswerHandler(req: Request, res: Response): Promise< const language = req.query.lang as string; const seq = req.params.seq; const seqAnswer = req.params.seqAnswer; - requireFields({ hruid }) + requireFields({ hruid }); const learningObjectId = getLearningObjectId(hruid, version, language); const questionId = getQuestionId(learningObjectId, seq); @@ -76,15 +76,13 @@ export async function deleteAnswerHandler(req: Request, res: Response): Promise< res.json({ answer }); } - - export async function updateAnswerHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; const version = req.params.version; const language = req.query.lang as string; const seq = req.params.seq; const seqAnswer = req.params.seqAnswer; - requireFields({ hruid }) + requireFields({ hruid }); const learningObjectId = getLearningObjectId(hruid, version, language); const questionId = getQuestionId(learningObjectId, seq); diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index e239d9ca..1158e041 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -1,15 +1,10 @@ import { Request, Response } from 'express'; -import { - createQuestion, - deleteQuestion, - getAllQuestions, - getQuestion, updateQuestion -} from '../services/questions.js'; -import {FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM} from '../config.js'; +import { createQuestion, deleteQuestion, getAllQuestions, getQuestion, updateQuestion } from '../services/questions.js'; +import { FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM } from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import {QuestionData, QuestionId} from '@dwengo-1/common/interfaces/question'; +import { QuestionData, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; -import {requireFields} from "./error-helper"; +import { requireFields } from './error-helper'; export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier { return { @@ -31,7 +26,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi const version = req.params.version; const language = req.query.lang as string; const full = req.query.full === 'true'; - requireFields({ hruid }) + requireFields({ hruid }); const learningObjectId = getLearningObjectId(hruid, version, language); @@ -45,7 +40,7 @@ export async function getQuestionHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; const version = req.params.version; const language = req.query.lang as string; - requireFields({ hruid }) + requireFields({ hruid }); const loId = getLearningObjectId(hruid, version, language); @@ -73,7 +67,6 @@ export async function createQuestionHandler(req: Request, res: Response): Promis const question = await createQuestion(loId, questionData); res.json({ question }); - } export async function deleteQuestionHandler(req: Request, res: Response): Promise { @@ -81,7 +74,7 @@ export async function deleteQuestionHandler(req: Request, res: Response): Promis const version = req.params.version; const language = req.query.lang as string; const seq = req.params.seq; - requireFields({ hruid }) + requireFields({ hruid }); const learningObjectId = getLearningObjectId(hruid, version, language); const questionId = getQuestionId(learningObjectId, seq); @@ -96,7 +89,7 @@ export async function updateQuestionHandler(req: Request, res: Response): Promis const version = req.params.version; const language = req.query.lang as string; const seq = req.params.seq; - requireFields({ hruid }) + requireFields({ hruid }); const learningObjectId = getLearningObjectId(hruid, version, language); const questionId = getQuestionId(learningObjectId, seq); diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index 94798248..93af06dd 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -21,7 +21,8 @@ export class AnswerRepository extends DwengoEntityRepository { } public async findAnswer(question: Question, sequenceNumber: number) { return this.findOne({ - toQuestion: question, sequenceNumber + toQuestion: question, + sequenceNumber, }); } public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { @@ -30,7 +31,7 @@ export class AnswerRepository extends DwengoEntityRepository { sequenceNumber: sequenceNumber, }); } - public async updateContent(answer: Answer, newContent: string){ + public async updateContent(answer: Answer, newContent: string) { answer.content = newContent; await this.save(answer); return answer; diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 31a856a6..b4e13ae9 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -62,12 +62,12 @@ export class QuestionRepository extends DwengoEntityRepository { }); } - public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number){ + public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number) { return this.findOne({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, learningObjectVersion: loId.version, - sequenceNumber + sequenceNumber, }); } diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts index 87464a0b..b78c579f 100644 --- a/backend/src/interfaces/answer.ts +++ b/backend/src/interfaces/answer.ts @@ -1,7 +1,7 @@ -import {mapToQuestionDTO, mapToQuestionDTOId} from './question.js'; +import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; import { Answer } from '../entities/questions/answer.entity.js'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; -import {mapToTeacherDTO} from "./teacher"; +import { mapToTeacherDTO } from './teacher'; /** * Convert a Question entity to a DTO format. diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 46f61825..40642ae7 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -1,10 +1,9 @@ import { Question } from '../entities/questions/question.entity.js'; -import { mapToStudentDTO} from './student.js'; -import { QuestionDTO, QuestionId} from '@dwengo-1/common/interfaces/question'; +import { mapToStudentDTO } from './student.js'; +import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; - function getLearningObjectIdentifier(question: Question): LearningObjectIdentifierDTO { return { hruid: question.learningObjectHruid, @@ -17,8 +16,8 @@ export function mapToLearningObjectID(loID: LearningObjectIdentifierDTO): Learni return { hruid: loID.hruid, language: loID.language, - version: loID.version ?? 1 - } + version: loID.version ?? 1, + }; } /** diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index edb27813..78b87061 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,19 +1,13 @@ -import express from "express"; -import { - createAnswerHandler, - deleteAnswerHandler, - getAnswerHandler, - getAnswersHandler, - updateAnswerHandler -} from "../controllers/answers"; +import express from 'express'; +import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAnswersHandler, updateAnswerHandler } from '../controllers/answers'; const router = express.Router({ mergeParams: true }); router.get('/', getAnswersHandler); -router.post('/', createAnswerHandler) +router.post('/', createAnswerHandler); -router.get('/:seqAnswer', getAnswerHandler) +router.get('/:seqAnswer', getAnswerHandler); router.delete('/:seqAnswer', deleteAnswerHandler); diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index ba39435c..e3651ba3 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -1,10 +1,5 @@ import express from 'express'; -import { - createQuestionHandler, - deleteQuestionHandler, - getAllQuestionsHandler, - getQuestionHandler, -} from '../controllers/questions.js'; +import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; import answerRoutes from './answers.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/services/answers.ts b/backend/src/services/answers.ts index 27b9cb13..8f3d6c9f 100644 --- a/backend/src/services/answers.ts +++ b/backend/src/services/answers.ts @@ -1,11 +1,11 @@ -import {getAnswerRepository} from "../data/repositories"; -import {Answer} from "../entities/questions/answer.entity"; -import {mapToAnswerDTO, mapToAnswerDTOId} from "../interfaces/answer"; -import {fetchTeacher} from "./teachers"; -import {fetchQuestion} from "./questions"; -import {QuestionId} from "@dwengo-1/common/interfaces/question"; -import {AnswerData, AnswerDTO, AnswerId} from "@dwengo-1/common/interfaces/answer"; -import {NotFoundException} from "../exceptions/not-found-exception"; +import { getAnswerRepository } from '../data/repositories'; +import { Answer } from '../entities/questions/answer.entity'; +import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer'; +import { fetchTeacher } from './teachers'; +import { fetchQuestion } from './questions'; +import { QuestionId } from '@dwengo-1/common/interfaces/question'; +import { AnswerData, AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; +import { NotFoundException } from '../exceptions/not-found-exception'; export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise { const answerRepository = getAnswerRepository(); @@ -27,7 +27,9 @@ export async function createAnswer(questionId: QuestionId, answerData: AnswerDat const content = answerData.content; const answer = await answerRepository.createAnswer({ - toQuestion, author, content + toQuestion, + author, + content, }); return mapToAnswerDTO(answer); } @@ -37,7 +39,7 @@ async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Prom const question = await fetchQuestion(questionId); const answer = await answerRepository.findAnswer(question, sequenceNumber); - if (!answer){ + if (!answer) { throw new NotFoundException('Answer with questionID and sequence number not found'); } @@ -59,7 +61,7 @@ export async function deleteAnswer(questionId: QuestionId, sequenceNumber: numbe return mapToAnswerDTO(answer); } -export async function updateAnswer(questionId: QuestionId, sequenceNumber: number, answerData: AnswerData){ +export async function updateAnswer(questionId: QuestionId, sequenceNumber: number, answerData: AnswerData) { const answerRepository = getAnswerRepository(); const answer = await fetchAnswer(questionId, sequenceNumber); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 21794ff5..5e5dd599 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -1,12 +1,12 @@ import { getQuestionRepository } from '../data/repositories.js'; -import {mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId} from '../interfaces/question.js'; +import { mapToLearningObjectID, mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; import { Question } from '../entities/questions/question.entity.js'; import { QuestionRepository } from '../data/questions/question-repository.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import {QuestionData, QuestionDTO, QuestionId} from '@dwengo-1/common/interfaces/question'; -import {NotFoundException} from "../exceptions/not-found-exception"; -import {FALLBACK_VERSION_NUM} from "../config"; -import {fetchStudent} from "./students"; +import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import { NotFoundException } from '../exceptions/not-found-exception'; +import { FALLBACK_VERSION_NUM } from '../config'; +import { fetchStudent } from './students'; export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { const questionRepository: QuestionRepository = getQuestionRepository(); @@ -26,7 +26,7 @@ export async function fetchQuestion(questionId: QuestionId): Promise { questionId.sequenceNumber ); - if (!question){ + if (!question) { throw new NotFoundException('Question with loID and sequence number not found'); } @@ -44,7 +44,9 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat const content = questionData.content; const question = await questionRepository.createQuestion({ - loId, author, content + loId, + author, + content, }); return mapToQuestionDTO(question); @@ -70,5 +72,3 @@ export async function updateQuestion(questionId: QuestionId, questionData: Quest const newQuestion = await questionRepository.updateContent(question, questionData.content); return mapToQuestionDTO(newQuestion); } - - diff --git a/common/src/interfaces/answer.ts b/common/src/interfaces/answer.ts index efba9437..8510ac7b 100644 --- a/common/src/interfaces/answer.ts +++ b/common/src/interfaces/answer.ts @@ -1,5 +1,5 @@ import { QuestionDTO, QuestionId } from './question'; -import {TeacherDTO} from "./teacher"; +import { TeacherDTO } from './teacher'; export interface AnswerDTO { author: TeacherDTO; From da1d502ee628595c4fdd0cc836de884da8d9a0ab Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 7 Apr 2025 11:30:15 +0200 Subject: [PATCH 06/14] feat: question tests --- backend/src/services/questions.ts | 9 +- backend/tests/controllers/questions.test.ts | 136 ++++++++++++++++++++ 2 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 backend/tests/controllers/questions.test.ts diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 2e26c960..a3c64b9f 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -54,10 +54,11 @@ export async function deleteQuestion(questionId: QuestionId): Promise { + let req: Partial; + let res: Partial; + + let jsonMock: Mock; + + beforeAll(async () => { + await setupTestApp(); + }); + + beforeEach(() => { + jsonMock = vi.fn(); + res = { + json: jsonMock, + }; + }); + + it('Get question list', async () => { + req = { + params: { hruid: 'id05', version: '1' }, + query: { lang: Language.English, full: 'true' }, + }; + + await getAllQuestionsHandler(req as Request, res as Response); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + // console.log(result.questions); + expect(result.questions).to.have.length.greaterThan(1); + }); + + it('Get question', async () => { + req = { + params: { hruid: 'id05', version: '1', seq: '1'}, + query: { lang: Language.English, full: 'true' }, + }; + + await getQuestionHandler(req as Request, res as Response); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); + + // const result = jsonMock.mock.lastCall?.[0]; + // console.log(result.question); + }) + + it('Get question with fallback sequence number and version', async () => { + req = { + params: { hruid: 'id05'}, + query: { lang: Language.English, full: 'true' }, + }; + + await getQuestionHandler(req as Request, res as Response); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); + + // const result = jsonMock.mock.lastCall?.[0]; + // console.log(result.question); + }) + + it('Get question with fallback sequence number and version', async () => { + req = { + params: { hruid: 'id05'}, + query: { lang: Language.English, full: 'true' }, + }; + + await getQuestionHandler(req as Request, res as Response); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); + + // const result = jsonMock.mock.lastCall?.[0]; + // console.log(result.question); + }) + + it('Get question hruid does not exist', async () => { + req = { + params: { hruid: 'id_not_exist'}, + query: { lang: Language.English, full: 'true' }, + }; + + await expect( async () => getQuestionHandler(req as Request, res as Response)) + .rejects.toThrow(NotFoundException); + }) + + it('Get question no hruid given', async () => { + req = { + params: {}, + query: { lang: Language.English, full: 'true' }, + }; + + await expect( async () => getQuestionHandler(req as Request, res as Response)) + .rejects.toThrow(BadRequestException); + }) + + /* + it('Create and delete question', async() => { + req = { + params: { hruid: 'id05', version: '1', seq: '2'}, + query: { lang: Language.English }, + }; + + await deleteQuestionHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + console.log(result.question); + }); + + */ + + it('Update question', async() => { + const newContent = "updated question"; + req = { + params: { hruid: 'id05', version: '1', seq: '1'}, + query: { lang: Language.English }, + body: { content: newContent } + }; + + await updateQuestionHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + // console.log(result.question); + expect(result.question.content).to.eq(newContent); + }); +}); From 42526900d36607f58ed87d4b975abc2b151b23a5 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 7 Apr 2025 11:46:09 +0200 Subject: [PATCH 07/14] feat: answer tests --- backend/src/controllers/answers.ts | 2 +- backend/src/routes/answers.ts | 4 +- backend/tests/controllers/answers.test.ts | 90 +++++++++++++++++++++ backend/tests/controllers/questions.test.ts | 13 --- 4 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 backend/tests/controllers/answers.test.ts diff --git a/backend/src/controllers/answers.ts b/backend/src/controllers/answers.ts index 1bb53019..f605b1a3 100644 --- a/backend/src/controllers/answers.ts +++ b/backend/src/controllers/answers.ts @@ -5,7 +5,7 @@ import {createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswe import {FALLBACK_SEQ_NUM} from "../config"; import {AnswerData} from "@dwengo-1/common/interfaces/answer"; -export async function getAnswersHandler(req: Request, res: Response): Promise { +export async function getAllAnswersHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; const version = req.params.version; const language = req.query.lang as string; diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index edb27813..a6280e49 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -3,13 +3,13 @@ import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, - getAnswersHandler, + getAllAnswersHandler, updateAnswerHandler } from "../controllers/answers"; const router = express.Router({ mergeParams: true }); -router.get('/', getAnswersHandler); +router.get('/', getAllAnswersHandler); router.post('/', createAnswerHandler) diff --git a/backend/tests/controllers/answers.test.ts b/backend/tests/controllers/answers.test.ts new file mode 100644 index 00000000..d357fd0c --- /dev/null +++ b/backend/tests/controllers/answers.test.ts @@ -0,0 +1,90 @@ +import {Request, Response} from "express"; +import {beforeAll, beforeEach, describe, expect, it, Mock, vi} from "vitest"; +import {setupTestApp} from "../setup-tests"; +import {Language} from "@dwengo-1/common/util/language"; +import {getAllAnswersHandler, getAnswerHandler, updateAnswerHandler} from "../../src/controllers/answers"; +import {BadRequestException} from "../../src/exceptions/bad-request-exception"; +import {NotFoundException} from "../../src/exceptions/not-found-exception"; + +describe('Questions controllers', () => { + let req: Partial; + let res: Partial; + + let jsonMock: Mock; + + beforeAll(async () => { + await setupTestApp(); + }); + + beforeEach(() => { + jsonMock = vi.fn(); + res = { + json: jsonMock, + }; + }); + + it('Get answers list', async () => { + req = { + params: {hruid: 'id05', version: '1', seq: '2'}, + query: {lang: Language.English, full: 'true'}, + }; + + await getAllAnswersHandler(req as Request, res as Response); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({answers: expect.anything()})); + + const result = jsonMock.mock.lastCall?.[0]; + // console.log(result.answers); + expect(result.questions).to.have.length.greaterThan(1); + }); + + it('Get answer', async () => { + req = { + params: {hruid: 'id05', version: '1', seq: '2', seqAnswer: '2'}, + query: {lang: Language.English, full: 'true'}, + }; + + await getAnswerHandler(req as Request, res as Response); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({answer: expect.anything()})); + + // const result = jsonMock.mock.lastCall?.[0]; + // console.log(result.answer); + }); + + it('Get answer hruid does not exist', async () => { + req = { + params: { hruid: 'id_not_exist'}, + query: { lang: Language.English, full: 'true' }, + }; + + await expect( async () => getAnswerHandler(req as Request, res as Response)) + .rejects.toThrow(NotFoundException); + }) + + it('Get answer no hruid given', async () => { + req = { + params: {}, + query: { lang: Language.English, full: 'true' }, + }; + + await expect( async () => getAnswerHandler(req as Request, res as Response)) + .rejects.toThrow(BadRequestException); + }) + + it('Update question', async() => { + const newContent = "updated question"; + req = { + params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2'}, + query: { lang: Language.English }, + body: { content: newContent } + }; + + await updateAnswerHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answer: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + // console.log(result.question); + expect(result.answer.content).to.eq(newContent); + }); + +}); diff --git a/backend/tests/controllers/questions.test.ts b/backend/tests/controllers/questions.test.ts index 3d9c6537..26fd588a 100644 --- a/backend/tests/controllers/questions.test.ts +++ b/backend/tests/controllers/questions.test.ts @@ -67,19 +67,6 @@ describe('Questions controllers', () => { // console.log(result.question); }) - it('Get question with fallback sequence number and version', async () => { - req = { - params: { hruid: 'id05'}, - query: { lang: Language.English, full: 'true' }, - }; - - await getQuestionHandler(req as Request, res as Response); - expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); - - // const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.question); - }) - it('Get question hruid does not exist', async () => { req = { params: { hruid: 'id_not_exist'}, From c07db3d999022212cc8f60cd7314993c36e2bb77 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 7 Apr 2025 11:53:47 +0200 Subject: [PATCH 08/14] fix: lint --- backend/src/data/questions/answer-repository.ts | 5 +++-- backend/src/data/questions/question-repository.ts | 3 ++- backend/src/services/answers.ts | 2 +- backend/src/services/questions.ts | 2 +- backend/tests/controllers/answers.test.ts | 8 ++++---- backend/tests/controllers/questions.test.ts | 14 +++++++------- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index 94798248..088ad623 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -2,6 +2,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Answer } from '../../entities/questions/answer.entity.js'; import { Question } from '../../entities/questions/question.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js'; +import {Loaded} from "@mikro-orm/core"; export class AnswerRepository extends DwengoEntityRepository { public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise { @@ -19,7 +20,7 @@ export class AnswerRepository extends DwengoEntityRepository { orderBy: { sequenceNumber: 'ASC' }, }); } - public async findAnswer(question: Question, sequenceNumber: number) { + public async findAnswer(question: Question, sequenceNumber: number): Promise | null> { return this.findOne({ toQuestion: question, sequenceNumber }); @@ -30,7 +31,7 @@ export class AnswerRepository extends DwengoEntityRepository { sequenceNumber: sequenceNumber, }); } - public async updateContent(answer: Answer, newContent: string){ + public async updateContent(answer: Answer, newContent: string): Promise { answer.content = newContent; await this.save(answer); return answer; diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 31a856a6..18bcd551 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -3,6 +3,7 @@ import { Question } from '../../entities/questions/question.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; +import {Loaded} from "@mikro-orm/core"; export class QuestionRepository extends DwengoEntityRepository { public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { @@ -62,7 +63,7 @@ export class QuestionRepository extends DwengoEntityRepository { }); } - public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number){ + public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise | null> { return this.findOne({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, diff --git a/backend/src/services/answers.ts b/backend/src/services/answers.ts index 27b9cb13..4844ff79 100644 --- a/backend/src/services/answers.ts +++ b/backend/src/services/answers.ts @@ -59,7 +59,7 @@ export async function deleteAnswer(questionId: QuestionId, sequenceNumber: numbe return mapToAnswerDTO(answer); } -export async function updateAnswer(questionId: QuestionId, sequenceNumber: number, answerData: AnswerData){ +export async function updateAnswer(questionId: QuestionId, sequenceNumber: number, answerData: AnswerData): Promise { const answerRepository = getAnswerRepository(); const answer = await fetchAnswer(questionId, sequenceNumber); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index a3c64b9f..71460691 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -52,7 +52,7 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat export async function deleteQuestion(questionId: QuestionId): Promise { const questionRepository = getQuestionRepository(); - const question = await fetchQuestion(questionId); // throws error if not found + const question = await fetchQuestion(questionId); // Throws error if not found const loId : LearningObjectIdentifier = { hruid: questionId.learningObjectIdentifier.hruid, diff --git a/backend/tests/controllers/answers.test.ts b/backend/tests/controllers/answers.test.ts index d357fd0c..26d7568c 100644 --- a/backend/tests/controllers/answers.test.ts +++ b/backend/tests/controllers/answers.test.ts @@ -33,7 +33,7 @@ describe('Questions controllers', () => { expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({answers: expect.anything()})); const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.answers); + // Console.log(result.answers); expect(result.questions).to.have.length.greaterThan(1); }); @@ -46,8 +46,8 @@ describe('Questions controllers', () => { await getAnswerHandler(req as Request, res as Response); expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({answer: expect.anything()})); - // const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.answer); + // Const result = jsonMock.mock.lastCall?.[0]; + // Console.log(result.answer); }); it('Get answer hruid does not exist', async () => { @@ -83,7 +83,7 @@ describe('Questions controllers', () => { expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answer: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.question); + // Console.log(result.question); expect(result.answer.content).to.eq(newContent); }); diff --git a/backend/tests/controllers/questions.test.ts b/backend/tests/controllers/questions.test.ts index 26fd588a..593445c0 100644 --- a/backend/tests/controllers/questions.test.ts +++ b/backend/tests/controllers/questions.test.ts @@ -37,7 +37,7 @@ describe('Questions controllers', () => { expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.questions); + // Console.log(result.questions); expect(result.questions).to.have.length.greaterThan(1); }); @@ -50,8 +50,8 @@ describe('Questions controllers', () => { await getQuestionHandler(req as Request, res as Response); expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); - // const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.question); + // Const result = jsonMock.mock.lastCall?.[0]; + // Console.log(result.question); }) it('Get question with fallback sequence number and version', async () => { @@ -63,8 +63,8 @@ describe('Questions controllers', () => { await getQuestionHandler(req as Request, res as Response); expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); - // const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.question); + // Const result = jsonMock.mock.lastCall?.[0]; + // Console.log(result.question); }) it('Get question hruid does not exist', async () => { @@ -88,7 +88,7 @@ describe('Questions controllers', () => { }) /* - it('Create and delete question', async() => { + It('Create and delete question', async() => { req = { params: { hruid: 'id05', version: '1', seq: '2'}, query: { lang: Language.English }, @@ -117,7 +117,7 @@ describe('Questions controllers', () => { expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ question: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; - // console.log(result.question); + // Console.log(result.question); expect(result.question.content).to.eq(newContent); }); }); From 09a11589d2f4cf4006c623ba7b9d3bbae4663f31 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 7 Apr 2025 16:30:52 +0200 Subject: [PATCH 09/14] feat: question answer frontend controller en queries --- backend/src/routes/questions.ts | 2 +- frontend/src/controllers/answers.ts | 40 +++++++++ frontend/src/controllers/base-controller.ts | 12 +-- frontend/src/controllers/questions.ts | 35 +++++++- frontend/src/queries/answers.ts | 57 +++++++++++++ frontend/src/queries/questions.ts | 91 +++++++++++++++++++++ 6 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 frontend/src/controllers/answers.ts create mode 100644 frontend/src/queries/answers.ts create mode 100644 frontend/src/queries/questions.ts diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index e3651ba3..5135c197 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -16,6 +16,6 @@ router.delete('/:seq', deleteQuestionHandler); // Information about a question with id router.get('/:seq', getQuestionHandler); -router.use('/:seq/answer', answerRoutes); +router.use('/:seq/answers', answerRoutes); export default router; diff --git a/frontend/src/controllers/answers.ts b/frontend/src/controllers/answers.ts new file mode 100644 index 00000000..60d623f8 --- /dev/null +++ b/frontend/src/controllers/answers.ts @@ -0,0 +1,40 @@ +import type {AnswerData, AnswerDTO, AnswerId} from "@dwengo-1/common/interfaces/answer"; +import {BaseController} from "@/controllers/base-controller.ts"; +import type {QuestionId} from "@dwengo-1/common/interfaces/question"; + + +export interface AnswersResponse { + answers: AnswerDTO[] | AnswerId[] +} + +export interface AnswerResponse { + answer: AnswerDTO +} + +export class AnswerController extends BaseController { + constructor(questionId: QuestionId) { + this.loId = questionId.learningObjectIdentifier; + this.sequenceNumber = questionId.sequenceNumber; + super(`learningObject/${loId.hruid}/:${loId.version}/questions/${this.sequenceNumber}/answers`) + } + + async getAll(full = true): Promise { + return this.get("/", {lang: this.loId.lang, full}); + } + + async getBy(seq: number): Promise { + return this.get(`/${seq}`, {lang: this.loId.lang}); + } + + async create(answerData: AnswerData) { + return this.post("/", answerData, {lang: this.loId.lang}); + } + + async remove(seq: number): Promise { + return this.delete(`/${seq}`, {lang: this.loId.lang}); + } + + async update(seq: number, answerData: AnswerData): Promise { + return this.put(`/${seq}`, answerData,{lang: this.loId.lang}); + } +} diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index 72d71819..f923c84f 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -21,20 +21,20 @@ export abstract class BaseController { return response.data; } - protected async post(path: string, body: unknown): Promise { - const response = await apiClient.post(this.absolutePathFor(path), body); + protected async post(path: string, body: unknown, queryParams?: QueryParams): Promise { + const response = await apiClient.post(this.absolutePathFor(path), body, { params: queryParams }); BaseController.assertSuccessResponse(response); return response.data; } - protected async delete(path: string): Promise { - const response = await apiClient.delete(this.absolutePathFor(path)); + protected async delete(path: string, queryParams?: QueryParams): Promise { + const response = await apiClient.delete(this.absolutePathFor(path), { params: queryParams} ); BaseController.assertSuccessResponse(response); return response.data; } - protected async put(path: string, body: unknown): Promise { - const response = await apiClient.put(this.absolutePathFor(path), body); + protected async put(path: string, body: unknown, queryParams?: QueryParams): Promise { + const response = await apiClient.put(this.absolutePathFor(path), body, { params: queryParams}); BaseController.assertSuccessResponse(response); return response.data; } diff --git a/frontend/src/controllers/questions.ts b/frontend/src/controllers/questions.ts index 9b0182de..d190c30f 100644 --- a/frontend/src/controllers/questions.ts +++ b/frontend/src/controllers/questions.ts @@ -1,5 +1,38 @@ -import type { QuestionDTO, QuestionId } from "@dwengo-1/interfaces/question"; +import type {QuestionData, QuestionDTO, QuestionId} from "@dwengo-1/common/interfaces/question"; +import {BaseController} from "@/controllers/base-controller.ts"; +import type {LearningObjectIdentifierDTO} from "@dwengo-1/common/interfaces/learning-content"; export interface QuestionsResponse { questions: QuestionDTO[] | QuestionId[]; } + +export interface QuestionResponse { + question: QuestionDTO; +} + +export class QuestionController extends BaseController { + constructor(loId: LearningObjectIdentifierDTO) { + this.loId = loId; + super(`learningObject/${loId.hruid}/:${loId.version}/questions`); + } + + async getAll(full = true): Promise { + return this.get("/", {lang: this.loId.lang, full}); + } + + async getBy(sequenceNumber: number): Promise { + return this.get(`/${sequenceNumber}`, {lang: this.loId.lang}); + } + + async create(questionData: QuestionData): Promise { + return this.post("/", questionData, {lang: this.loId.lang}) + } + + async remove(sequenceNumber: number) { + return this.delete(`/${sequenceNumber}`, {lang: this.loId.lang}); + } + + async update(sequenceNumber: number, questionData: QuestionData) { + return this.put(`/${sequenceNumber}`, questionData, {lang: this.loId.lang}); + } +} diff --git a/frontend/src/queries/answers.ts b/frontend/src/queries/answers.ts new file mode 100644 index 00000000..4f599963 --- /dev/null +++ b/frontend/src/queries/answers.ts @@ -0,0 +1,57 @@ +import type { QuestionId} from "@dwengo-1/common/dist/interfaces/question.ts"; +import { type MaybeRefOrGetter, toValue} from "vue"; +import { + useMutation, + type UseMutationReturnType, + useQuery, + type UseQueryReturnType +} from "@tanstack/vue-query"; +import {AnswerController, type AnswerResponse, type AnswersResponse} from "@/controllers/answers.ts"; +import type {AnswerData} from "@dwengo-1/common/dist/interfaces/answer.ts"; + +// TODO caching + +export function useAnswersQuery( + questionId: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + return useQuery({ + queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)), + enabled: () => Boolean(toValue(questionId)), + }); +} + +export function useAnswerQuery( + questionId: MaybeRefOrGetter, + sequenceNumber: MaybeRefOrGetter +): UseQueryReturnType { + return useQuery({ + queryFn: async () => new AnswerController(toValue(questionId)).getBy(toValue(sequenceNumber)), + enabled: () => Boolean(toValue(questionId)), + }); +} + +export function useCreateAnswerMutation( + questionId: MaybeRefOrGetter, +): UseMutationReturnType { + return useMutation({ + mutationFn: async (data) => new AnswerController(toValue(questionId)).create(data), + }); +} + +export function useDeleteAnswerMutation( + questionId: MaybeRefOrGetter, +): UseMutationReturnType { + return useMutation({ + mutationFn: async (seq) => new AnswerController(toValue(questionId)).remove(seq), + }); +} + +export function useUpdateAnswerMutation( + questionId: MaybeRefOrGetter, +): UseMutationReturnType { + return useMutation({ + mutationFn: async (data, seq) => new AnswerController(toValue(questionId)).update(seq, data), + }); +} + diff --git a/frontend/src/queries/questions.ts b/frontend/src/queries/questions.ts new file mode 100644 index 00000000..fa6553de --- /dev/null +++ b/frontend/src/queries/questions.ts @@ -0,0 +1,91 @@ +import {QuestionController, type QuestionResponse, type QuestionsResponse} from "@/controllers/questions.ts"; +import type {QuestionData, QuestionId} from "@dwengo-1/common/interfaces/question"; +import type {LearningObjectIdentifierDTO} from "@dwengo-1/common/interfaces/learning-content"; +import {computed, type MaybeRefOrGetter, toValue} from "vue"; +import { + useMutation, + type UseMutationReturnType, + useQuery, + useQueryClient, + type UseQueryReturnType +} from "@tanstack/vue-query"; + + +export function questionsQueryKey(loId: LearningObjectIdentifierDTO, full: boolean): [string, string, number, string, boolean] { + return ["questions", loId.hruid, loId.version, loId.language, full]; +} + +export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] { + const loId = questionId.learningObjectIdentifier; + return ["question", loId.hruid, loId.version, loId.language, questionId.sequenceNumber]; +} + +export function useQuestionsQuery( + loId: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => questionsQueryKey(toValue(loId), toValue(full))), + queryFn: async () => new QuestionController(toValue(loId)).getAll(toValue(full)), + enabled: () => Boolean(toValue(loId)), + }); +} + +export function useQuestionQuery( + questionId: MaybeRefOrGetter, +): UseQueryReturnType { + const loId = toValue(questionId).learningObjectIdentifier; + const sequenceNumber = toValue(questionId).sequenceNumber; + return useQuery({ + queryKey: computed(() => questionQueryKey(loId, sequenceNumber)), + queryFn: async () => new QuestionController(loId).getBy(sequenceNumber), + enabled: () => Boolean(toValue(questionId)), + }); +} + +export function useCreateQuestionMutation( + loId: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data) => new QuestionController(toValue(loId)).create(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); + await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); + }, + }); +} + +export function useUpdateQuestionMutation( + questionId: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const loId = toValue(questionId).learningObjectIdentifier; + const sequenceNumber = toValue(questionId).sequenceNumber; + + return useMutation({ + mutationFn: async ( data ) => new QuestionController(loId).update(sequenceNumber, data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); + await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); + await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) }); + }, + }); +} + +export function useDeleteQuestionMutation( + questionId: MaybeRefOrGetter, +): UseMutationReturnType { + const queryClient = useQueryClient(); + const loId = toValue(questionId).learningObjectIdentifier; + const sequenceNumber = toValue(questionId).sequenceNumber; + return useMutation({ + mutationFn: async () => new QuestionController(loId).remove(sequenceNumber), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); + await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); + await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) }); + }, + }); +} From 5cea6929f9a37e61f62c6487b091d90e608f9592 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 7 Apr 2025 16:34:12 +0200 Subject: [PATCH 10/14] fix: linter --- backend/src/data/questions/answer-repository.ts | 2 +- backend/src/data/questions/question-repository.ts | 2 +- frontend/src/controllers/answers.ts | 2 +- frontend/src/controllers/questions.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index 088ad623..1b353b28 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -20,7 +20,7 @@ export class AnswerRepository extends DwengoEntityRepository { orderBy: { sequenceNumber: 'ASC' }, }); } - public async findAnswer(question: Question, sequenceNumber: number): Promise | null> { + public async findAnswer(question: Question, sequenceNumber: number): Promise | null> { return this.findOne({ toQuestion: question, sequenceNumber }); diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 18bcd551..27a4ac19 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -63,7 +63,7 @@ export class QuestionRepository extends DwengoEntityRepository { }); } - public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise | null> { + public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise | null> { return this.findOne({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, diff --git a/frontend/src/controllers/answers.ts b/frontend/src/controllers/answers.ts index 60d623f8..b95f27db 100644 --- a/frontend/src/controllers/answers.ts +++ b/frontend/src/controllers/answers.ts @@ -26,7 +26,7 @@ export class AnswerController extends BaseController { return this.get(`/${seq}`, {lang: this.loId.lang}); } - async create(answerData: AnswerData) { + async create(answerData: AnswerData): Promise { return this.post("/", answerData, {lang: this.loId.lang}); } diff --git a/frontend/src/controllers/questions.ts b/frontend/src/controllers/questions.ts index d190c30f..28609e95 100644 --- a/frontend/src/controllers/questions.ts +++ b/frontend/src/controllers/questions.ts @@ -28,11 +28,11 @@ export class QuestionController extends BaseController { return this.post("/", questionData, {lang: this.loId.lang}) } - async remove(sequenceNumber: number) { + async remove(sequenceNumber: number): Promise { return this.delete(`/${sequenceNumber}`, {lang: this.loId.lang}); } - async update(sequenceNumber: number, questionData: QuestionData) { + async update(sequenceNumber: number, questionData: QuestionData): Promise { return this.put(`/${sequenceNumber}`, questionData, {lang: this.loId.lang}); } } From a8895cc429914e9354836da1f522a4417aaca955 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Mon, 7 Apr 2025 14:44:59 +0000 Subject: [PATCH 11/14] style: fix linting issues met Prettier --- .../src/data/questions/answer-repository.ts | 5 +- .../src/data/questions/question-repository.ts | 4 +- backend/src/routes/answers.ts | 14 ++---- backend/src/services/answers.ts | 22 +++++---- backend/src/services/questions.ts | 6 +-- backend/tests/controllers/answers.test.ts | 47 +++++++++---------- backend/tests/controllers/questions.test.ts | 46 ++++++++---------- frontend/src/controllers/answers.ts | 23 +++++---- frontend/src/controllers/base-controller.ts | 4 +- frontend/src/controllers/questions.ts | 16 +++---- frontend/src/queries/answers.ts | 20 +++----- frontend/src/queries/questions.ts | 18 +++---- 12 files changed, 104 insertions(+), 121 deletions(-) diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index 1b353b28..54f67a01 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -2,7 +2,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Answer } from '../../entities/questions/answer.entity.js'; import { Question } from '../../entities/questions/question.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js'; -import {Loaded} from "@mikro-orm/core"; +import { Loaded } from '@mikro-orm/core'; export class AnswerRepository extends DwengoEntityRepository { public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise { @@ -22,7 +22,8 @@ export class AnswerRepository extends DwengoEntityRepository { } public async findAnswer(question: Question, sequenceNumber: number): Promise | null> { return this.findOne({ - toQuestion: question, sequenceNumber + toQuestion: question, + sequenceNumber, }); } public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 27a4ac19..cf9ecab0 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -3,7 +3,7 @@ import { Question } from '../../entities/questions/question.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; -import {Loaded} from "@mikro-orm/core"; +import { Loaded } from '@mikro-orm/core'; export class QuestionRepository extends DwengoEntityRepository { public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { @@ -68,7 +68,7 @@ export class QuestionRepository extends DwengoEntityRepository { learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, learningObjectVersion: loId.version, - sequenceNumber + sequenceNumber, }); } diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index a6280e49..5800733a 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,19 +1,13 @@ -import express from "express"; -import { - createAnswerHandler, - deleteAnswerHandler, - getAnswerHandler, - getAllAnswersHandler, - updateAnswerHandler -} from "../controllers/answers"; +import express from 'express'; +import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers'; const router = express.Router({ mergeParams: true }); router.get('/', getAllAnswersHandler); -router.post('/', createAnswerHandler) +router.post('/', createAnswerHandler); -router.get('/:seqAnswer', getAnswerHandler) +router.get('/:seqAnswer', getAnswerHandler); router.delete('/:seqAnswer', deleteAnswerHandler); diff --git a/backend/src/services/answers.ts b/backend/src/services/answers.ts index 4844ff79..f2abad51 100644 --- a/backend/src/services/answers.ts +++ b/backend/src/services/answers.ts @@ -1,11 +1,11 @@ -import {getAnswerRepository} from "../data/repositories"; -import {Answer} from "../entities/questions/answer.entity"; -import {mapToAnswerDTO, mapToAnswerDTOId} from "../interfaces/answer"; -import {fetchTeacher} from "./teachers"; -import {fetchQuestion} from "./questions"; -import {QuestionId} from "@dwengo-1/common/interfaces/question"; -import {AnswerData, AnswerDTO, AnswerId} from "@dwengo-1/common/interfaces/answer"; -import {NotFoundException} from "../exceptions/not-found-exception"; +import { getAnswerRepository } from '../data/repositories'; +import { Answer } from '../entities/questions/answer.entity'; +import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer'; +import { fetchTeacher } from './teachers'; +import { fetchQuestion } from './questions'; +import { QuestionId } from '@dwengo-1/common/interfaces/question'; +import { AnswerData, AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; +import { NotFoundException } from '../exceptions/not-found-exception'; export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise { const answerRepository = getAnswerRepository(); @@ -27,7 +27,9 @@ export async function createAnswer(questionId: QuestionId, answerData: AnswerDat const content = answerData.content; const answer = await answerRepository.createAnswer({ - toQuestion, author, content + toQuestion, + author, + content, }); return mapToAnswerDTO(answer); } @@ -37,7 +39,7 @@ async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Prom const question = await fetchQuestion(questionId); const answer = await answerRepository.findAnswer(question, sequenceNumber); - if (!answer){ + if (!answer) { throw new NotFoundException('Answer with questionID and sequence number not found'); } diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 64bf4471..d2e6440e 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -56,11 +56,11 @@ export async function deleteQuestion(questionId: QuestionId): Promise { let req: Partial; @@ -25,12 +25,12 @@ describe('Questions controllers', () => { it('Get answers list', async () => { req = { - params: {hruid: 'id05', version: '1', seq: '2'}, - query: {lang: Language.English, full: 'true'}, + params: { hruid: 'id05', version: '1', seq: '2' }, + query: { lang: Language.English, full: 'true' }, }; await getAllAnswersHandler(req as Request, res as Response); - expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({answers: expect.anything()})); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answers: expect.anything() })); const result = jsonMock.mock.lastCall?.[0]; // Console.log(result.answers); @@ -39,12 +39,12 @@ describe('Questions controllers', () => { it('Get answer', async () => { req = { - params: {hruid: 'id05', version: '1', seq: '2', seqAnswer: '2'}, - query: {lang: Language.English, full: 'true'}, + params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2' }, + query: { lang: Language.English, full: 'true' }, }; await getAnswerHandler(req as Request, res as Response); - expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({answer: expect.anything()})); + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ answer: expect.anything() })); // Const result = jsonMock.mock.lastCall?.[0]; // Console.log(result.answer); @@ -52,13 +52,12 @@ describe('Questions controllers', () => { it('Get answer hruid does not exist', async () => { req = { - params: { hruid: 'id_not_exist'}, + params: { hruid: 'id_not_exist' }, query: { lang: Language.English, full: 'true' }, }; - await expect( async () => getAnswerHandler(req as Request, res as Response)) - .rejects.toThrow(NotFoundException); - }) + await expect(async () => getAnswerHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); + }); it('Get answer no hruid given', async () => { req = { @@ -66,16 +65,15 @@ describe('Questions controllers', () => { query: { lang: Language.English, full: 'true' }, }; - await expect( async () => getAnswerHandler(req as Request, res as Response)) - .rejects.toThrow(BadRequestException); - }) + await expect(async () => getAnswerHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException); + }); - it('Update question', async() => { - const newContent = "updated question"; + it('Update question', async () => { + const newContent = 'updated question'; req = { - params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2'}, + params: { hruid: 'id05', version: '1', seq: '2', seqAnswer: '2' }, query: { lang: Language.English }, - body: { content: newContent } + body: { content: newContent }, }; await updateAnswerHandler(req as Request, res as Response); @@ -86,5 +84,4 @@ describe('Questions controllers', () => { // Console.log(result.question); expect(result.answer.content).to.eq(newContent); }); - }); diff --git a/backend/tests/controllers/questions.test.ts b/backend/tests/controllers/questions.test.ts index 593445c0..f2612fc7 100644 --- a/backend/tests/controllers/questions.test.ts +++ b/backend/tests/controllers/questions.test.ts @@ -1,14 +1,10 @@ -import {beforeAll, beforeEach, describe, expect, it, Mock, vi} from "vitest"; -import {Request, Response} from "express"; -import {setupTestApp} from "../setup-tests"; -import { - getAllQuestionsHandler, - getQuestionHandler, updateQuestionHandler -} from "../../src/controllers/questions"; -import {Language} from "@dwengo-1/common/util/language"; -import {NotFoundException} from "../../src/exceptions/not-found-exception"; -import {BadRequestException} from "../../src/exceptions/bad-request-exception"; - +import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { Request, Response } from 'express'; +import { setupTestApp } from '../setup-tests'; +import { getAllQuestionsHandler, getQuestionHandler, updateQuestionHandler } from '../../src/controllers/questions'; +import { Language } from '@dwengo-1/common/util/language'; +import { NotFoundException } from '../../src/exceptions/not-found-exception'; +import { BadRequestException } from '../../src/exceptions/bad-request-exception'; describe('Questions controllers', () => { let req: Partial; @@ -43,7 +39,7 @@ describe('Questions controllers', () => { it('Get question', async () => { req = { - params: { hruid: 'id05', version: '1', seq: '1'}, + params: { hruid: 'id05', version: '1', seq: '1' }, query: { lang: Language.English, full: 'true' }, }; @@ -52,11 +48,11 @@ describe('Questions controllers', () => { // Const result = jsonMock.mock.lastCall?.[0]; // Console.log(result.question); - }) + }); it('Get question with fallback sequence number and version', async () => { req = { - params: { hruid: 'id05'}, + params: { hruid: 'id05' }, query: { lang: Language.English, full: 'true' }, }; @@ -65,17 +61,16 @@ describe('Questions controllers', () => { // Const result = jsonMock.mock.lastCall?.[0]; // Console.log(result.question); - }) + }); it('Get question hruid does not exist', async () => { req = { - params: { hruid: 'id_not_exist'}, + params: { hruid: 'id_not_exist' }, query: { lang: Language.English, full: 'true' }, }; - await expect( async () => getQuestionHandler(req as Request, res as Response)) - .rejects.toThrow(NotFoundException); - }) + await expect(async () => getQuestionHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); + }); it('Get question no hruid given', async () => { req = { @@ -83,9 +78,8 @@ describe('Questions controllers', () => { query: { lang: Language.English, full: 'true' }, }; - await expect( async () => getQuestionHandler(req as Request, res as Response)) - .rejects.toThrow(BadRequestException); - }) + await expect(async () => getQuestionHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException); + }); /* It('Create and delete question', async() => { @@ -104,12 +98,12 @@ describe('Questions controllers', () => { */ - it('Update question', async() => { - const newContent = "updated question"; + it('Update question', async () => { + const newContent = 'updated question'; req = { - params: { hruid: 'id05', version: '1', seq: '1'}, + params: { hruid: 'id05', version: '1', seq: '1' }, query: { lang: Language.English }, - body: { content: newContent } + body: { content: newContent }, }; await updateQuestionHandler(req as Request, res as Response); diff --git a/frontend/src/controllers/answers.ts b/frontend/src/controllers/answers.ts index b95f27db..04235e77 100644 --- a/frontend/src/controllers/answers.ts +++ b/frontend/src/controllers/answers.ts @@ -1,40 +1,39 @@ -import type {AnswerData, AnswerDTO, AnswerId} from "@dwengo-1/common/interfaces/answer"; -import {BaseController} from "@/controllers/base-controller.ts"; -import type {QuestionId} from "@dwengo-1/common/interfaces/question"; - +import type { AnswerData, AnswerDTO, AnswerId } from "@dwengo-1/common/interfaces/answer"; +import { BaseController } from "@/controllers/base-controller.ts"; +import type { QuestionId } from "@dwengo-1/common/interfaces/question"; export interface AnswersResponse { - answers: AnswerDTO[] | AnswerId[] + answers: AnswerDTO[] | AnswerId[]; } export interface AnswerResponse { - answer: AnswerDTO + answer: AnswerDTO; } export class AnswerController extends BaseController { constructor(questionId: QuestionId) { this.loId = questionId.learningObjectIdentifier; this.sequenceNumber = questionId.sequenceNumber; - super(`learningObject/${loId.hruid}/:${loId.version}/questions/${this.sequenceNumber}/answers`) + super(`learningObject/${loId.hruid}/:${loId.version}/questions/${this.sequenceNumber}/answers`); } async getAll(full = true): Promise { - return this.get("/", {lang: this.loId.lang, full}); + return this.get("/", { lang: this.loId.lang, full }); } async getBy(seq: number): Promise { - return this.get(`/${seq}`, {lang: this.loId.lang}); + return this.get(`/${seq}`, { lang: this.loId.lang }); } async create(answerData: AnswerData): Promise { - return this.post("/", answerData, {lang: this.loId.lang}); + return this.post("/", answerData, { lang: this.loId.lang }); } async remove(seq: number): Promise { - return this.delete(`/${seq}`, {lang: this.loId.lang}); + return this.delete(`/${seq}`, { lang: this.loId.lang }); } async update(seq: number, answerData: AnswerData): Promise { - return this.put(`/${seq}`, answerData,{lang: this.loId.lang}); + return this.put(`/${seq}`, answerData, { lang: this.loId.lang }); } } diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index f923c84f..54cf6221 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -28,13 +28,13 @@ export abstract class BaseController { } protected async delete(path: string, queryParams?: QueryParams): Promise { - const response = await apiClient.delete(this.absolutePathFor(path), { params: queryParams} ); + const response = await apiClient.delete(this.absolutePathFor(path), { params: queryParams }); BaseController.assertSuccessResponse(response); return response.data; } protected async put(path: string, body: unknown, queryParams?: QueryParams): Promise { - const response = await apiClient.put(this.absolutePathFor(path), body, { params: queryParams}); + const response = await apiClient.put(this.absolutePathFor(path), body, { params: queryParams }); BaseController.assertSuccessResponse(response); return response.data; } diff --git a/frontend/src/controllers/questions.ts b/frontend/src/controllers/questions.ts index 28609e95..60a51d1a 100644 --- a/frontend/src/controllers/questions.ts +++ b/frontend/src/controllers/questions.ts @@ -1,6 +1,6 @@ -import type {QuestionData, QuestionDTO, QuestionId} from "@dwengo-1/common/interfaces/question"; -import {BaseController} from "@/controllers/base-controller.ts"; -import type {LearningObjectIdentifierDTO} from "@dwengo-1/common/interfaces/learning-content"; +import type { QuestionData, QuestionDTO, QuestionId } from "@dwengo-1/common/interfaces/question"; +import { BaseController } from "@/controllers/base-controller.ts"; +import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; export interface QuestionsResponse { questions: QuestionDTO[] | QuestionId[]; @@ -17,22 +17,22 @@ export class QuestionController extends BaseController { } async getAll(full = true): Promise { - return this.get("/", {lang: this.loId.lang, full}); + return this.get("/", { lang: this.loId.lang, full }); } async getBy(sequenceNumber: number): Promise { - return this.get(`/${sequenceNumber}`, {lang: this.loId.lang}); + return this.get(`/${sequenceNumber}`, { lang: this.loId.lang }); } async create(questionData: QuestionData): Promise { - return this.post("/", questionData, {lang: this.loId.lang}) + return this.post("/", questionData, { lang: this.loId.lang }); } async remove(sequenceNumber: number): Promise { - return this.delete(`/${sequenceNumber}`, {lang: this.loId.lang}); + return this.delete(`/${sequenceNumber}`, { lang: this.loId.lang }); } async update(sequenceNumber: number, questionData: QuestionData): Promise { - return this.put(`/${sequenceNumber}`, questionData, {lang: this.loId.lang}); + return this.put(`/${sequenceNumber}`, questionData, { lang: this.loId.lang }); } } diff --git a/frontend/src/queries/answers.ts b/frontend/src/queries/answers.ts index 4f599963..f2d0f9c4 100644 --- a/frontend/src/queries/answers.ts +++ b/frontend/src/queries/answers.ts @@ -1,13 +1,8 @@ -import type { QuestionId} from "@dwengo-1/common/dist/interfaces/question.ts"; -import { type MaybeRefOrGetter, toValue} from "vue"; -import { - useMutation, - type UseMutationReturnType, - useQuery, - type UseQueryReturnType -} from "@tanstack/vue-query"; -import {AnswerController, type AnswerResponse, type AnswersResponse} from "@/controllers/answers.ts"; -import type {AnswerData} from "@dwengo-1/common/dist/interfaces/answer.ts"; +import type { QuestionId } from "@dwengo-1/common/dist/interfaces/question.ts"; +import { type MaybeRefOrGetter, toValue } from "vue"; +import { useMutation, type UseMutationReturnType, useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { AnswerController, type AnswerResponse, type AnswersResponse } from "@/controllers/answers.ts"; +import type { AnswerData } from "@dwengo-1/common/dist/interfaces/answer.ts"; // TODO caching @@ -23,7 +18,7 @@ export function useAnswersQuery( export function useAnswerQuery( questionId: MaybeRefOrGetter, - sequenceNumber: MaybeRefOrGetter + sequenceNumber: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ queryFn: async () => new AnswerController(toValue(questionId)).getBy(toValue(sequenceNumber)), @@ -49,9 +44,8 @@ export function useDeleteAnswerMutation( export function useUpdateAnswerMutation( questionId: MaybeRefOrGetter, -): UseMutationReturnType { +): UseMutationReturnType { return useMutation({ mutationFn: async (data, seq) => new AnswerController(toValue(questionId)).update(seq, data), }); } - diff --git a/frontend/src/queries/questions.ts b/frontend/src/queries/questions.ts index fa6553de..b69164a4 100644 --- a/frontend/src/queries/questions.ts +++ b/frontend/src/queries/questions.ts @@ -1,17 +1,19 @@ -import {QuestionController, type QuestionResponse, type QuestionsResponse} from "@/controllers/questions.ts"; -import type {QuestionData, QuestionId} from "@dwengo-1/common/interfaces/question"; -import type {LearningObjectIdentifierDTO} from "@dwengo-1/common/interfaces/learning-content"; -import {computed, type MaybeRefOrGetter, toValue} from "vue"; +import { QuestionController, type QuestionResponse, type QuestionsResponse } from "@/controllers/questions.ts"; +import type { QuestionData, QuestionId } from "@dwengo-1/common/interfaces/question"; +import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; +import { computed, type MaybeRefOrGetter, toValue } from "vue"; import { useMutation, type UseMutationReturnType, useQuery, useQueryClient, - type UseQueryReturnType + type UseQueryReturnType, } from "@tanstack/vue-query"; - -export function questionsQueryKey(loId: LearningObjectIdentifierDTO, full: boolean): [string, string, number, string, boolean] { +export function questionsQueryKey( + loId: LearningObjectIdentifierDTO, + full: boolean, +): [string, string, number, string, boolean] { return ["questions", loId.hruid, loId.version, loId.language, full]; } @@ -65,7 +67,7 @@ export function useUpdateQuestionMutation( const sequenceNumber = toValue(questionId).sequenceNumber; return useMutation({ - mutationFn: async ( data ) => new QuestionController(loId).update(sequenceNumber, data), + mutationFn: async (data) => new QuestionController(loId).update(sequenceNumber, data), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) }); await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) }); From 399986f263ed43ae52d8814a5b09b78f9d3b3fca Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 7 Apr 2025 16:46:57 +0200 Subject: [PATCH 12/14] fix: typ fout in test --- backend/tests/controllers/answers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/controllers/answers.test.ts b/backend/tests/controllers/answers.test.ts index 26d7568c..081cc176 100644 --- a/backend/tests/controllers/answers.test.ts +++ b/backend/tests/controllers/answers.test.ts @@ -34,7 +34,7 @@ describe('Questions controllers', () => { const result = jsonMock.mock.lastCall?.[0]; // Console.log(result.answers); - expect(result.questions).to.have.length.greaterThan(1); + expect(result.answers).to.have.length.greaterThan(1); }); it('Get answer', async () => { From c10fc8db17ec20635348bd4c35dcc49339362719 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Mon, 7 Apr 2025 15:57:31 +0000 Subject: [PATCH 13/14] style: fix linting issues met Prettier --- frontend/src/controllers/questions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/controllers/questions.ts b/frontend/src/controllers/questions.ts index 979ecb7c..60a51d1a 100644 --- a/frontend/src/controllers/questions.ts +++ b/frontend/src/controllers/questions.ts @@ -2,7 +2,6 @@ import type { QuestionData, QuestionDTO, QuestionId } from "@dwengo-1/common/int import { BaseController } from "@/controllers/base-controller.ts"; import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; - export interface QuestionsResponse { questions: QuestionDTO[] | QuestionId[]; } From 62973472069cf9aa88e3abdc4b9c024fff741b6f Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 9 Apr 2025 10:26:33 +0200 Subject: [PATCH 14/14] fix: .js toevoegen aan imports --- backend/src/controllers/answers.ts | 8 ++++---- backend/src/controllers/questions.ts | 2 +- backend/src/interfaces/answer.ts | 2 +- backend/src/routes/answers.ts | 2 +- backend/src/services/answers.ts | 12 ++++++------ backend/src/services/questions.ts | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/src/controllers/answers.ts b/backend/src/controllers/answers.ts index eaf51749..38cebe84 100644 --- a/backend/src/controllers/answers.ts +++ b/backend/src/controllers/answers.ts @@ -1,8 +1,8 @@ import { Request, Response } from 'express'; -import { requireFields } from './error-helper'; -import { getLearningObjectId, getQuestionId } from './questions'; -import { createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer } from '../services/answers'; -import { FALLBACK_SEQ_NUM } from '../config'; +import { requireFields } from './error-helper.js'; +import { getLearningObjectId, getQuestionId } from './questions.js'; +import { createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer } from '../services/answers.js'; +import { FALLBACK_SEQ_NUM } from '../config.js'; import { AnswerData } from '@dwengo-1/common/interfaces/answer'; export async function getAllAnswersHandler(req: Request, res: Response): Promise { diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 1158e041..dfb3ef6c 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -4,7 +4,7 @@ import { FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM } from '../config import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionData, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; -import { requireFields } from './error-helper'; +import { requireFields } from './error-helper.js'; export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier { return { diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts index b78c579f..513fc63e 100644 --- a/backend/src/interfaces/answer.ts +++ b/backend/src/interfaces/answer.ts @@ -1,7 +1,7 @@ import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; import { Answer } from '../entities/questions/answer.entity.js'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; -import { mapToTeacherDTO } from './teacher'; +import { mapToTeacherDTO } from './teacher.js'; /** * Convert a Question entity to a DTO format. diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index 5800733a..b74f76a0 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers'; +import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/services/answers.ts b/backend/src/services/answers.ts index f2abad51..ab603883 100644 --- a/backend/src/services/answers.ts +++ b/backend/src/services/answers.ts @@ -1,11 +1,11 @@ -import { getAnswerRepository } from '../data/repositories'; -import { Answer } from '../entities/questions/answer.entity'; -import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer'; -import { fetchTeacher } from './teachers'; -import { fetchQuestion } from './questions'; +import { getAnswerRepository } from '../data/repositories.js'; +import { Answer } from '../entities/questions/answer.entity.js'; +import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; +import { fetchTeacher } from './teachers.js'; +import { fetchQuestion } from './questions.js'; import { QuestionId } from '@dwengo-1/common/interfaces/question'; import { AnswerData, AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; -import { NotFoundException } from '../exceptions/not-found-exception'; +import { NotFoundException } from '../exceptions/not-found-exception.js'; export async function getAnswersByQuestion(questionId: QuestionId, full: boolean): Promise { const answerRepository = getAnswerRepository(); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index d2e6440e..2dd7da1c 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -4,9 +4,9 @@ import { Question } from '../entities/questions/question.entity.js'; import { QuestionRepository } from '../data/questions/question-repository.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; -import { NotFoundException } from '../exceptions/not-found-exception'; -import { FALLBACK_VERSION_NUM } from '../config'; -import { fetchStudent } from './students'; +import { NotFoundException } from '../exceptions/not-found-exception.js'; +import { FALLBACK_VERSION_NUM } from '../config.js'; +import { fetchStudent } from './students.js'; export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { const questionRepository: QuestionRepository = getQuestionRepository();