Skip to content

Commit 8a4c0d4

Browse files
Merge pull request #293 from DevLoversTeam/sl/feat/quiz
(SP: 3) [Backend] Redis Cache for Quiz Questions
2 parents c5a91f9 + 1b2abb7 commit 8a4c0d4

5 files changed

Lines changed: 178 additions & 197 deletions

File tree

frontend/app/api/quiz/[slug]/route.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.

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: 152 additions & 13 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,11 +40,15 @@ export async function getOrCreateQuizAnswersCache(
2340
return true;
2441
}
2542

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

28-
const existing = await redis.get<QuizAnswersCache>(key);
29-
if (existing) {
30-
return true;
45+
try {
46+
const existing = await redis.get<QuizAnswersCache>(key);
47+
if (existing) {
48+
return true;
49+
}
50+
} catch (err) {
51+
console.warn('Redis cache read failed:', err);
3152
}
3253

3354
const correctAnswers = await db
@@ -56,8 +77,13 @@ export async function getOrCreateQuizAnswersCache(
5677
cachedAt: Date.now(),
5778
};
5879

59-
await redis.set(key, cacheData);
60-
return true;
80+
try {
81+
await redis.set(key, cacheData);
82+
} catch (err) {
83+
console.warn('Failed to cache quiz answers in Redis', err);
84+
}
85+
return true;
86+
6187
}
6288

6389
export async function getCorrectAnswer(
@@ -67,10 +93,14 @@ export async function getCorrectAnswer(
6793
const redis = getRedisClient();
6894

6995
if (redis) {
70-
const key = getCacheKey(quizId);
71-
const cache = await redis.get<QuizAnswersCache>(key);
72-
if (cache) {
73-
return cache.answers[questionId] ?? null;
96+
try {
97+
const key = getAnswersCacheKey(quizId);
98+
const cache = await redis.get<QuizAnswersCache>(key);
99+
if (cache) {
100+
return cache.answers[questionId] ?? null;
101+
}
102+
} catch (err) {
103+
console.warn('Redis cache read failed, falling back to DB:', err);
74104
}
75105
}
76106

@@ -89,3 +119,112 @@ export async function getCorrectAnswer(
89119

90120
return result[0]?.answerId ?? null;
91121
}
122+
123+
export async function getOrCreateQuestionsCache(
124+
quizId: string,
125+
locale: string
126+
): Promise<QuizQuestionWithAnswers[] | null> {
127+
const redis = getRedisClient();
128+
if (!redis) {
129+
return null;
130+
}
131+
132+
const key = getQuestionsCacheKey(quizId, locale);
133+
134+
try {
135+
const existing = await redis.get<QuizQuestionsCache>(key);
136+
if (existing) {
137+
return existing.questions;
138+
}
139+
} catch (err) {
140+
console.warn('Redis cache read failed, falling back to DB:', err);
141+
}
142+
143+
const questionsData = await db
144+
.select({
145+
id: quizQuestions.id,
146+
displayOrder: quizQuestions.displayOrder,
147+
difficulty: quizQuestions.difficulty,
148+
questionText: quizQuestionContent.questionText,
149+
explanation: quizQuestionContent.explanation,
150+
})
151+
.from(quizQuestions)
152+
.leftJoin(
153+
quizQuestionContent,
154+
and(
155+
eq(quizQuestionContent.quizQuestionId, quizQuestions.id),
156+
eq(quizQuestionContent.locale, locale)
157+
)
158+
)
159+
.where(eq(quizQuestions.quizId, quizId))
160+
.orderBy(quizQuestions.displayOrder);
161+
162+
if (questionsData.length === 0) {
163+
const cacheData: QuizQuestionsCache = {
164+
quizId,
165+
locale,
166+
questions: [],
167+
cachedAt: Date.now(),
168+
};
169+
try {
170+
await redis.set(key, cacheData);
171+
} catch (e) {
172+
console.warn('Redis cache write failed:', e);
173+
}
174+
return [];
175+
}
176+
177+
const questionIds = questionsData.map(q => q.id);
178+
179+
const allAnswers = await db
180+
.select({
181+
id: quizAnswers.id,
182+
questionId: quizAnswers.quizQuestionId,
183+
displayOrder: quizAnswers.displayOrder,
184+
isCorrect: quizAnswers.isCorrect,
185+
answerText: quizAnswerTranslations.answerText,
186+
})
187+
.from(quizAnswers)
188+
.leftJoin(
189+
quizAnswerTranslations,
190+
and(
191+
eq(quizAnswerTranslations.quizAnswerId, quizAnswers.id),
192+
eq(quizAnswerTranslations.locale, locale)
193+
)
194+
)
195+
.where(inArray(quizAnswers.quizQuestionId, questionIds))
196+
.orderBy(quizAnswers.displayOrder);
197+
198+
const answersByQuestion = new Map<string, typeof allAnswers>();
199+
for (const answer of allAnswers) {
200+
const arr = answersByQuestion.get(answer.questionId) || [];
201+
arr.push(answer);
202+
answersByQuestion.set(answer.questionId, arr);
203+
}
204+
205+
206+
const questions: QuizQuestionWithAnswers[] = questionsData.map(q => ({
207+
...q,
208+
answers: (answersByQuestion.get(q.id) || []).map(a => ({
209+
id: a.id,
210+
displayOrder: a.displayOrder,
211+
isCorrect: a.isCorrect,
212+
answerText: a.answerText,
213+
})),
214+
}));
215+
216+
const cacheData: QuizQuestionsCache = {
217+
quizId,
218+
locale,
219+
questions,
220+
cachedAt: Date.now(),
221+
};
222+
223+
try {
224+
await redis.set(key, cacheData);
225+
} catch (e) {
226+
console.warn('Redis cache write failed:', e);
227+
}
228+
229+
return questions;
230+
}

0 commit comments

Comments
 (0)