Skip to content

Commit 4c8a20b

Browse files
committed
get match items draft
1 parent 397de48 commit 4c8a20b

File tree

8 files changed

+304
-11
lines changed

8 files changed

+304
-11
lines changed

backend/matching/.env.local

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ EXPRESS_PORT=9004
55
MATCHING_DB_HOSTNAME="localhost"
66
MATCHING_DB_PORT=6379
77

8+
USER_SERVER_ENDPOINT="http://localhost:9003"
9+
QUESTION_SERVER_ENDPOINT="http://localhost:9002"
10+
COLLAB_SERVER_ENDPOINT="http://localhost:9004"
11+
12+
813
# MATCHING_DB_USERNAME="peerprep-match-express"
914
# MATCHING_DB_PASSWORD="G7jBgyz9wGAFQ5La"
1015
# REDIS_ARGS="--requirepass G7jBgyz9wGAFQ5La --user ${MATCHING_DB_USERNAME} on >G7jBgyz9wGAFQ5La ~* allcommands --user default off nopass nocommands"
Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,140 @@
1-
export const getMatchItems = () => {
2-
// TODO: Get Match Items
3-
return {
4-
roomId: '',
5-
questionId: '',
6-
};
1+
import { IMatchType } from "@/types";
2+
import axios from 'axios';
3+
4+
interface IGetRandomQuestionPayload {
5+
attemptedQuestions: number[];
6+
difficulty?: string;
7+
topic?: string;
8+
}
9+
10+
interface IQuestion {
11+
id: number;
12+
title: string;
13+
description: string;
14+
difficulty: string;
15+
topic: string[];
16+
}
17+
18+
interface IServiceResponse<T> {
19+
success: boolean;
20+
data?: T;
21+
error?: { message: string };
22+
}
23+
24+
interface IMatchItemsResponse {
25+
roomId: string;
26+
questionId: number;
27+
question: IQuestion;
28+
}
29+
30+
export const getMatchItems = async (
31+
searchIdentifier: IMatchType,
32+
topic?: string,
33+
difficulty?: string,
34+
userId1?: string,
35+
userId2?: string
36+
): Promise<IServiceResponse<IMatchItemsResponse>> => {
37+
const userEndpoint = `${process.env.USER_SERVER_ENDPOINT}`;
38+
const questionEndpoint = `${process.env.QUESTION_SERVER_ENDPOINT}`;
39+
const collabServerEndpoint = `${process.env.COLLAB_SERVER_ENDPOINT}`;
40+
41+
try {
42+
if (!userId1 || !userId2) {
43+
throw new Error('Both user IDs are required');
44+
}
45+
46+
// Fetch attempted questions for both users
47+
const [attemptedQuestions1, attemptedQuestions2] = await Promise.all([
48+
fetchAttemptedQuestions(userEndpoint, userId1),
49+
fetchAttemptedQuestions(userEndpoint, userId2)
50+
]);
51+
52+
// Combine attempted questions from both users
53+
const allAttemptedQuestions = [...new Set([...attemptedQuestions1, ...attemptedQuestions2])];
54+
55+
// Prepare payload for the /random endpoint
56+
const payload: IGetRandomQuestionPayload = {
57+
attemptedQuestions: allAttemptedQuestions,
58+
};
59+
60+
if (searchIdentifier === 'difficulty' && difficulty) {
61+
payload.difficulty = difficulty;
62+
} else if (searchIdentifier === 'topic' && topic) {
63+
payload.topic = topic;
64+
} else if (searchIdentifier === 'exact match' && topic && difficulty) {
65+
payload.topic = topic;
66+
payload.difficulty = difficulty;
67+
}
68+
69+
// Query the question endpoint using the /random endpoint
70+
const questionResponse = await axios.post<IServiceResponse<{ question: IQuestion }>>(
71+
`${questionEndpoint}/random`,
72+
payload
73+
);
74+
75+
if (!questionResponse.data.success || !questionResponse.data.data?.question) {
76+
throw new Error(questionResponse.data.error?.message || 'Failed to get a random question');
77+
}
78+
79+
const questionId = questionResponse.data.data.question.id;
80+
81+
// Update attempted questions for both users
82+
await Promise.all([
83+
updateAttemptedQuestions(userEndpoint, userId1, questionId),
84+
updateAttemptedQuestions(userEndpoint, userId2, questionId)
85+
]);
86+
87+
// Query the collab server for the room ID
88+
const roomResponse = await axios.get<IServiceResponse<{ roomId: string }>>(
89+
`${collabServerEndpoint}/rooms`,
90+
{
91+
params: {
92+
userId1,
93+
userId2,
94+
questionId: questionId.toString(),
95+
}
96+
}
97+
);
98+
99+
if (!roomResponse.data.success || !roomResponse.data.data?.roomId) {
100+
throw new Error(roomResponse.data.error?.message || 'Failed to create room');
101+
}
102+
103+
return {
104+
success: true,
105+
data: {
106+
roomId: roomResponse.data.data.roomId,
107+
questionId: questionId,
108+
question: questionResponse.data.data.question,
109+
}
110+
};
111+
} catch (error) {
112+
console.error('Error in getMatchItems:', error);
113+
return {
114+
success: false,
115+
error: {
116+
message: error instanceof Error ? error.message : 'An unknown error occurred',
117+
}
118+
};
119+
}
7120
};
121+
122+
async function fetchAttemptedQuestions(userEndpoint: string, userId: string): Promise<number[]> {
123+
const response = await axios.get<IServiceResponse<{ attemptedQuestions: number[] }>>(
124+
`${userEndpoint}/user/${userId}/attempted-questions`
125+
);
126+
if (!response.data.success || !response.data.data?.attemptedQuestions) {
127+
throw new Error(`Failed to fetch attempted questions for user ${userId}`);
128+
}
129+
return response.data.data.attemptedQuestions;
130+
}
131+
132+
async function updateAttemptedQuestions(userEndpoint: string, userId: string, questionId: number): Promise<void> {
133+
const response = await axios.post<IServiceResponse<{ message: string }>>(
134+
`${userEndpoint}/user/${userId}/attempted-question`,
135+
{ questionId }
136+
);
137+
if (!response.data.success) {
138+
throw new Error(`Failed to update attempted questions for user ${userId}`);
139+
}
140+
}

backend/matching/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ export type IStreamMessage = {
2828
};
2929
value?: Awaited<ReturnType<(typeof client)['ft']['search']>>['documents'][number]['value'];
3030
};
31+
32+
export type IMatchType = 'difficulty' | 'topic' | 'exact match' | undefined

backend/matching/src/workers/matcher.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { POOL_INDEX, STREAM_GROUP, STREAM_NAME, STREAM_WORKER } from '@/lib/db/c
33
import { decodePoolTicket, getPoolKey, getStreamId } from '@/lib/utils';
44
import { getMatchItems } from '@/services';
55
import { MATCH_SVC_EVENT } from '@/ws';
6+
import { IMatchType } from '@/types';
67

78
import { connectClient, sendNotif } from './common';
89

@@ -41,7 +42,9 @@ async function processMatch(
4142
redisClient: typeof client,
4243
{ requestorUserId, requestorStreamId, requestorSocketPort }: RequestorParams,
4344
matches: Awaited<ReturnType<(typeof client)['ft']['search']>>,
44-
searchIdentifier?: string
45+
searchIdentifier?: IMatchType,
46+
topic?: string,
47+
difficulty?: string
4548
) {
4649
if (matches.total > 0) {
4750
for (const matched of matches.documents) {
@@ -61,6 +64,7 @@ async function processMatch(
6164
const matchedStreamId = getStreamId(timestamp);
6265

6366
logger.info(`Found match: ${JSON.stringify(matched)}`);
67+
6468

6569
await Promise.all([
6670
// Remove other from pool
@@ -70,7 +74,7 @@ async function processMatch(
7074
]);
7175

7276
// Notify both sockets
73-
const { ...matchItems } = getMatchItems();
77+
const { ...matchItems } = getMatchItems(searchIdentifier, topic, difficulty, requestorUserId, matchedUserId);
7478
sendNotif([requestorSocketPort, matchedSocketPort], MATCH_SVC_EVENT.SUCCESS, matchItems);
7579
sendNotif([requestorSocketPort, matchedSocketPort], MATCH_SVC_EVENT.DISCONNECT);
7680

@@ -142,7 +146,9 @@ async function match() {
142146
redisClient,
143147
requestorParams,
144148
exactMatches,
145-
'exact match'
149+
'exact match',
150+
topic,
151+
difficulty
146152
);
147153

148154
if (exactMatchFound || !topic || !difficulty) {
@@ -160,7 +166,9 @@ async function match() {
160166
redisClient,
161167
requestorParams,
162168
topicMatches,
163-
'topic'
169+
'topic',
170+
topic,
171+
difficulty
164172
);
165173

166174
if (topicMatchFound) {
@@ -177,7 +185,9 @@ async function match() {
177185
redisClient,
178186
requestorParams,
179187
difficultyMatches,
180-
'difficulty'
188+
'difficulty',
189+
topic,
190+
difficulty
181191
);
182192

183193
if (!hasDifficultyMatch) {

backend/question/src/services/get/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type IGetRandomQuestionPayload = {
5151

5252
export type IGetRandomQuestionResponse = IServiceResponse<{
5353
question: {
54+
id: number; // question's unique identifier or number
5455
title: string; // name or title of the question
5556
description: string; // question description
5657
difficulty: string; // difficulty level (e.g., 'easy', 'medium', 'hard')
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { StatusCodes } from 'http-status-codes';
2+
import type { IRouteHandler } from '@/types';
3+
import { getAttemptedQuestionsService, addAttemptedQuestionService } from '@/services/questions';
4+
5+
export const addAttemptedQuestion: IRouteHandler = async (req, res) => {
6+
const userId = req.params.userId; // Assuming the userId is passed as a route parameter
7+
const { questionId } = req.body; // Assuming the questionId is passed in the request body
8+
9+
if (!userId || !questionId) {
10+
return res.status(StatusCodes.BAD_REQUEST).json('User ID and Question ID are required');
11+
}
12+
13+
const { code, data, error } = await addAttemptedQuestionService(userId, questionId);
14+
15+
if (error || code !== StatusCodes.OK || !data) {
16+
const sanitizedErr = error?.message ?? 'An error occurred.';
17+
return res.status(code).json(sanitizedErr);
18+
}
19+
20+
return res.status(StatusCodes.OK).json(data);
21+
};
22+
23+
export const getAttemptedQuestions: IRouteHandler = async (req, res) => {
24+
const userId = req.params.userId; // Assuming the userId is passed as a route parameter
25+
26+
if (!userId) {
27+
return res.status(StatusCodes.BAD_REQUEST).json('User ID is required');
28+
}
29+
30+
const { code, data, error } = await getAttemptedQuestionsService(userId);
31+
32+
if (error || code !== StatusCodes.OK || !data) {
33+
const sanitizedErr = error?.message ?? 'An error occurred.';
34+
return res.status(code).json(sanitizedErr);
35+
}
36+
37+
return res.status(StatusCodes.OK).json(data);
38+
};

backend/user/src/routes/auth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import express from 'express';
22

33
import { checkEmailValid, checkUsernameValid, login, logout, register } from '@/controllers/auth';
44
import { limiter } from '@/lib/ratelimit';
5+
import { getAttemptedQuestions, addAttemptedQuestion } from '@/controllers/questions';
56

67
const router = express.Router();
78

@@ -10,6 +11,9 @@ router.post('/logout', logout);
1011
router.post('/register', register);
1112
router.post('/username-valid', checkUsernameValid);
1213
router.post('/email-valid', checkEmailValid);
14+
15+
router.post('/attempted-questions', getAttemptedQuestions)
16+
router.post('/attempt-question', addAttemptedQuestion)
1317
router.use(limiter);
1418

1519
export default router;

0 commit comments

Comments
 (0)