Skip to content

Commit f39fe37

Browse files
committed
feat(quiz): add Redis cache for quiz questions
- Create types/quiz.ts with shared quiz domain types - Add getOrCreateQuestionsCache() to cache questions per quiz/locale - Integrate cache into getQuizQuestions() with DB fallback
1 parent c7f55d4 commit f39fe37

3 files changed

Lines changed: 138 additions & 23 deletions

File tree

frontend/db/queries/quiz.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { and, desc, eq, inArray, sql } from 'drizzle-orm';
22
import { unstable_cache } from 'next/cache';
33
import { cache } from 'react';
44

5+
import { getOrCreateQuestionsCache } from '@/lib/quiz/quiz-answers-redis';
6+
import type { QuizQuestionWithAnswers } from '@/types/quiz';
7+
58
import { db } from '../index';
69
import { categories, categoryTranslations } from '../schema/categories';
710
import {
@@ -14,6 +17,7 @@ import {
1417
quizTranslations,
1518
quizzes,
1619
} from '../schema/quiz';
20+
export type { QuizAnswer, QuizQuestion, QuizQuestionWithAnswers } from '@/types/quiz';
1721

1822
export interface Quiz {
1923
id: string;
@@ -27,24 +31,6 @@ export interface Quiz {
2731
categoryName: string | null;
2832
}
2933

30-
export interface QuizQuestion {
31-
id: string;
32-
displayOrder: number;
33-
difficulty: string | null;
34-
questionText: string | null;
35-
explanation: any;
36-
}
37-
38-
export interface QuizAnswer {
39-
id: string;
40-
displayOrder: number;
41-
isCorrect: boolean;
42-
answerText: string | null;
43-
}
44-
45-
export interface QuizQuestionWithAnswers extends QuizQuestion {
46-
answers: QuizAnswer[];
47-
}
4834

4935
export interface QuizAnswerClient {
5036
id: string;
@@ -194,6 +180,10 @@ export async function getQuizQuestions(
194180
quizId: string,
195181
locale: string = 'uk'
196182
): Promise<QuizQuestionWithAnswers[]> {
183+
const cached = await getOrCreateQuestionsCache(quizId, locale);
184+
if (cached !== null) {
185+
return cached;
186+
}
197187
const questionsData = await db
198188
.select({
199189
id: quizQuestions.id,

frontend/lib/quiz/quiz-answers-redis.ts

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
1-
import { and, eq } from 'drizzle-orm';
1+
import { and, eq,inArray } from 'drizzle-orm';
22

33
import { db } from '@/db';
4-
import { quizAnswers, quizQuestions } from '@/db/schema/quiz';
4+
import {
5+
quizAnswers,
6+
quizAnswerTranslations,
7+
quizQuestionContent,
8+
quizQuestions} from '@/db/schema/quiz';
59
import { getRedisClient } from '@/lib/redis';
10+
import type { QuizQuestionWithAnswers } from '@/types/quiz';
611

712
interface QuizAnswersCache {
813
quizId: string;
914
answers: Record<string, string>;
1015
cachedAt: number;
1116
}
1217

13-
function getCacheKey(quizId: string): string {
18+
interface QuizQuestionsCache {
19+
quizId: string;
20+
locale: string;
21+
questions: QuizQuestionWithAnswers[];
22+
cachedAt: number;
23+
}
24+
25+
function getQuestionsCacheKey(quizId: string, locale: string): string {
26+
return `quiz:questions:${quizId}:${locale}`;
27+
}
28+
29+
30+
function getAnswersCacheKey(quizId: string): string {
1431
return `quiz:answers:${quizId}`;
1532
}
1633

@@ -23,7 +40,7 @@ export async function getOrCreateQuizAnswersCache(
2340
return true;
2441
}
2542

26-
const key = getCacheKey(quizId);
43+
const key = getAnswersCacheKey(quizId);
2744

2845
const existing = await redis.get<QuizAnswersCache>(key);
2946
if (existing) {
@@ -67,7 +84,7 @@ export async function getCorrectAnswer(
6784
const redis = getRedisClient();
6885

6986
if (redis) {
70-
const key = getCacheKey(quizId);
87+
const key = getAnswersCacheKey(quizId);
7188
const cache = await redis.get<QuizAnswersCache>(key);
7289
if (cache) {
7390
return cache.answers[questionId] ?? null;
@@ -89,3 +106,93 @@ export async function getCorrectAnswer(
89106

90107
return result[0]?.answerId ?? null;
91108
}
109+
110+
export async function getOrCreateQuestionsCache(
111+
quizId: string,
112+
locale: string
113+
): Promise<QuizQuestionWithAnswers[] | null> {
114+
const redis = getRedisClient();
115+
if (!redis) {
116+
return null;
117+
}
118+
119+
const key = getQuestionsCacheKey(quizId, locale);
120+
121+
const existing = await redis.get<QuizQuestionsCache>(key);
122+
if (existing) {
123+
return existing.questions;
124+
}
125+
126+
const questionsData = await db
127+
.select({
128+
id: quizQuestions.id,
129+
displayOrder: quizQuestions.displayOrder,
130+
difficulty: quizQuestions.difficulty,
131+
questionText: quizQuestionContent.questionText,
132+
explanation: quizQuestionContent.explanation,
133+
})
134+
.from(quizQuestions)
135+
.leftJoin(
136+
quizQuestionContent,
137+
and(
138+
eq(quizQuestionContent.quizQuestionId, quizQuestions.id),
139+
eq(quizQuestionContent.locale, locale)
140+
)
141+
)
142+
.where(eq(quizQuestions.quizId, quizId))
143+
.orderBy(quizQuestions.displayOrder);
144+
145+
if (questionsData.length === 0) {
146+
return [];
147+
}
148+
149+
const questionIds = questionsData.map(q => q.id);
150+
151+
const allAnswers = await db
152+
.select({
153+
id: quizAnswers.id,
154+
questionId: quizAnswers.quizQuestionId,
155+
displayOrder: quizAnswers.displayOrder,
156+
isCorrect: quizAnswers.isCorrect,
157+
answerText: quizAnswerTranslations.answerText,
158+
})
159+
.from(quizAnswers)
160+
.leftJoin(
161+
quizAnswerTranslations,
162+
and(
163+
eq(quizAnswerTranslations.quizAnswerId, quizAnswers.id),
164+
eq(quizAnswerTranslations.locale, locale)
165+
)
166+
)
167+
.where(inArray(quizAnswers.quizQuestionId, questionIds))
168+
.orderBy(quizAnswers.displayOrder);
169+
170+
const answersByQuestion = new Map<string, typeof allAnswers>();
171+
for (const answer of allAnswers) {
172+
const arr = answersByQuestion.get(answer.questionId) || [];
173+
arr.push(answer);
174+
answersByQuestion.set(answer.questionId, arr);
175+
}
176+
177+
178+
const questions: QuizQuestionWithAnswers[] = questionsData.map(q => ({
179+
...q,
180+
answers: (answersByQuestion.get(q.id) || []).map(a => ({
181+
id: a.id,
182+
displayOrder: a.displayOrder,
183+
isCorrect: a.isCorrect,
184+
answerText: a.answerText,
185+
})),
186+
}));
187+
188+
const cacheData: QuizQuestionsCache = {
189+
quizId,
190+
locale,
191+
questions,
192+
cachedAt: Date.now(),
193+
};
194+
195+
await redis.set(key, cacheData);
196+
197+
return questions;
198+
}

frontend/types/quiz.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface QuizQuestion {
2+
id: string;
3+
displayOrder: number;
4+
difficulty: string | null;
5+
questionText: string | null;
6+
explanation: any;
7+
}
8+
9+
export interface QuizAnswer {
10+
id: string;
11+
displayOrder: number;
12+
isCorrect: boolean;
13+
answerText: string | null;
14+
}
15+
16+
export interface QuizQuestionWithAnswers extends QuizQuestion {
17+
answers: QuizAnswer[];
18+
}

0 commit comments

Comments
 (0)