From 09a11589d2f4cf4006c623ba7b9d3bbae4663f31 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Mon, 7 Apr 2025 16:30:52 +0200 Subject: [PATCH] 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)) }); + }, + }); +}