Skip to content

Commit b161602

Browse files
committed
Shift All History API Calls to UseCase Files
1 parent d16b0d7 commit b161602

File tree

10 files changed

+159
-87
lines changed

10 files changed

+159
-87
lines changed

backend/question-service/controllers/historyController.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,18 @@ export const getUserHistoryEntries = async (req: Request, res: Response) => {
5151
model: 'category',
5252
},
5353
});
54-
res.status(200).json(historyEntries);
54+
const historyViewModels = historyEntries.map((entry) => {
55+
return {
56+
id: entry._id,
57+
key: entry._id,
58+
attemptStartedAt: entry.attemptStartedAt.getTime(),
59+
attemptCompletedAt: entry.attemptCompletedAt.getTime(),
60+
title: entry.question.title,
61+
difficulty: entry.question.difficulty,
62+
topics: entry.question.categories.map((cat: any) => cat.name),
63+
attemptCode: entry.attemptCode,
64+
}});
65+
res.status(200).json(historyViewModels);
5566
} catch (error) {
5667
res.status(500).json({ error: getErrorMessage(error) });
5768
}

backend/question-service/models/HistoryEntry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Question } from './Question';
44
export interface HistoryEntry extends mongoose.Document {
55
_id: Types.ObjectId;
66
userId: string;
7-
question: Types.ObjectId | Question;
7+
question: Question;
88
roomId: string; // Note: This should not be used to retrieve the room from matching service! This is only here to serve as a uniqueness check for updating attempt information!
99
attemptStartedAt: Date;
1010
attemptCompletedAt: Date;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
import { HistoryEntry } from "domain/entities/HistoryEntry";
3+
import { BaseApi } from "../../infrastructure/Api/BaseApi";
4+
5+
const API_URL = "/api/history";
6+
7+
export class HistoryRemoteDataSource extends BaseApi {
8+
constructor() {
9+
super(API_URL);
10+
}
11+
12+
async getAllUserHistories(): Promise<HistoryEntry[]> {
13+
return await this.get<HistoryEntry[]>("/");
14+
}
15+
16+
async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise<void> {
17+
return await this.protectedPost<void>("/", { questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode });
18+
}
19+
20+
async deleteSelectedHistories(selectedHistoryIds: string[]): Promise<void> {
21+
selectedHistoryIds.forEach(async (id) => {
22+
await this.delete<void>(`/${id}`);
23+
});
24+
}
25+
26+
async deleteAllUserHistories(): Promise<void> {
27+
await this.delete<void>("/all");
28+
}
29+
}
30+
31+
export const historyRemoteDataSource = new HistoryRemoteDataSource();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { HistoryEntry } from "domain/entities/HistoryEntry";
2+
import { historyRemoteDataSource } from "./HistoryRemoteDataSource";
3+
4+
export class HistoryRepositoryImpl {
5+
private dataSource = historyRemoteDataSource;
6+
7+
async getAllUserHistories(): Promise<HistoryEntry[]> {
8+
return this.dataSource.getAllUserHistories();
9+
}
10+
11+
async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise<void> {
12+
this.dataSource.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode)
13+
}
14+
15+
async deleteSelectedUserHistories(selectedHistoryIds: string[]): Promise<void> {
16+
this.dataSource.deleteSelectedHistories(selectedHistoryIds);
17+
}
18+
19+
async deleteAllUserHistories(): Promise<void> {
20+
this.dataSource.deleteAllUserHistories();
21+
}
22+
}
23+
24+
export const historyRepository = new HistoryRepositoryImpl();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface HistoryEntry {
2+
_id: string;
3+
key: string;
4+
attemptStartedAt: string;
5+
attemptCompletedAt: string;
6+
title: string;
7+
difficulty: 'Easy' | 'Medium' | 'Hard';
8+
topics: string[];
9+
attemptCode: string;
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { HistoryEntry } from "domain/entities/HistoryEntry";
2+
3+
export interface IHistoryRepository {
4+
getAllUserHistories(): Promise<HistoryEntry[]>;
5+
createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise<void>;
6+
deleteSelectedUserHistories(selectedHistoryIds: string[]): Promise<void>;
7+
deleteAllUserHistories(): Promise<void>;
8+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { historyRepository } from "data/repositories/HistoryRepositoryImpl";
2+
import { HistoryEntry } from "domain/entities/HistoryEntry";
3+
import { IHistoryRepository } from "domain/repositories/IHistoryRepository";
4+
5+
export class HistoryUseCases {
6+
constructor(private historyRepository: IHistoryRepository) {}
7+
8+
/**
9+
* Retrieves all categories.
10+
* @returns Promise resolving to an array of Category objects.
11+
*/
12+
async getAllCategories(): Promise<HistoryEntry[]> {
13+
const allHistories = this.historyRepository.getAllUserHistories();
14+
return allHistories;
15+
}
16+
17+
/**
18+
* Creates a new User History Entry. If there already exists a History Entry with the same userId (obtained in
19+
* backend via JWT token and not passed here) and roomId, update the existing one instead.
20+
* (Note that just roomId isn't enough because the collaborator will also have a very similar History Entry)
21+
* @returns Promise resolving when the history has been successfully created.
22+
*/
23+
async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise<void> {
24+
if (!questionId || questionId.trim() === ""
25+
|| !roomId || roomId.trim() === ""
26+
|| !attemptStartedAt || attemptStartedAt.trim() === ""
27+
|| !attemptCompletedAt || attemptCompletedAt.trim() === ""
28+
|| !collaboratorId || collaboratorId.trim() === "") {
29+
throw new Error("Missing Attempt Details");
30+
}
31+
await this.historyRepository.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode);
32+
}
33+
34+
/**
35+
* Deletes histories by their unique _id.
36+
* @param selectedHistoryIds - The unique identifiers of the histories to delete.
37+
* @returns Promise resolving when the histories are successfully deleted.
38+
* @throws Error if selectedHistoryIds is of length 0 or contains an empty _id.
39+
*/
40+
async deleteSelectedUserHistories(selectedHistoryIds: string[]): Promise<void> {
41+
if (selectedHistoryIds.length === 0 || !selectedHistoryIds.every((_id) => (!_id || _id.trim() === ""))) {
42+
throw new Error("History ID must be provided");
43+
}
44+
await this.historyRepository.deleteSelectedUserHistories(selectedHistoryIds);
45+
}
46+
47+
/**
48+
* Deletes all histories owned by the logged in user. Requires user to be logged in
49+
* with valid JWT token registered.
50+
* @returns Promise resolving when the histories are successfully deleted.
51+
*/
52+
async deleteAllUserHistories(): Promise<void> {
53+
await this.historyRepository.deleteAllUserHistories();
54+
}
55+
}
56+
57+
export const historyUseCases = new HistoryUseCases(historyRepository);

frontend/src/infrastructure/Api/BaseApi.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ export class BaseApi {
1717
}
1818

1919
private createAxiosInstance(baseUrl: string): AxiosInstance {
20+
const token = AuthClientStore.getAccessToken();
2021
return axios.create({
2122
baseURL: API_URL + baseUrl,
2223
timeout: 10000,
2324
headers: {
24-
"Content-Type": "application/json"
25+
"Content-Type": "application/json",
26+
'Authorization': `Bearer ${token}`,
2527
},
2628
withCredentials: true
2729
});

frontend/src/presentation/components/CodeEditor/LeaveButton.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import React, { useState } from "react";
22
import { Button, Modal } from "antd";
33
import { StopOutlined } from "@ant-design/icons";
44
import styles from "./LeaveButton.module.css";
5-
import AuthClientStore from "data/auth/AuthClientStore";
65
import { useNavigate } from "react-router-dom";
6+
import { historyUseCases } from "domain/usecases/HistoryUseCases";
77

88
interface LeaveButtonProps {
99
getEditorText: () => string;
@@ -31,22 +31,14 @@ const LeaveButton: React.FC<LeaveButtonProps> = ({
3131
};
3232

3333
const handleOk = async () => {
34-
const token = AuthClientStore.getAccessToken();
35-
await fetch('http://localhost:3002/api/history', {
36-
method: 'POST',
37-
headers: {
38-
'Content-Type': 'application/json',
39-
'Authorization': `Bearer ${token}`,
40-
},
41-
body: JSON.stringify({
34+
await historyUseCases.createOrUpdateUserHistory(
4235
questionId,
4336
roomId,
44-
attemptStartedAt,
45-
attemptCompletedAt: new Date(),
37+
attemptStartedAt.getTime().toString(),
38+
Date.now().toString(),
4639
collaboratorId,
47-
attemptCode: getEditorText(),
48-
})
49-
});
40+
getEditorText(),
41+
);
5042
navigate('/')
5143
setIsModalVisible(false);
5244
};

frontend/src/presentation/components/RecentAttemptsTable.tsx

Lines changed: 7 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,8 @@ import React, { useEffect, useState } from "react";
22
import { Table, Tag, Typography, Badge, Button, message, Empty } from 'antd';
33
import type { ColumnsType } from 'antd/es/table';
44
import styles from "./RecentAttemptsTable.module.css";
5-
import AuthClientStore from "data/auth/AuthClientStore";
6-
7-
interface HistoryEntry {
8-
id: string;
9-
key: string;
10-
date: string;
11-
title: string;
12-
difficulty: 'Easy' | 'Medium' | 'Hard';
13-
topics: string[];
14-
}
5+
import { HistoryEntry } from "domain/entities/HistoryEntry";
6+
import { historyUseCases } from "domain/usecases/HistoryUseCases";
157

168
export const RecentAttemptsTable: React.FC = () => {
179
const [recentAttemptsData, setRecentAttemptsData] = useState<HistoryEntry[]>([]);
@@ -25,33 +17,8 @@ export const RecentAttemptsTable: React.FC = () => {
2517
const fetchRecentAttempts = async () => {
2618
setLoading(true);
2719
try {
28-
const token = AuthClientStore.getAccessToken();
29-
const response = await fetch('http://localhost:3002/api/history', {
30-
method: 'GET',
31-
headers: {
32-
'Content-Type': 'application/json',
33-
'Authorization': `Bearer ${token}`,
34-
},
35-
});
36-
37-
if (!response.ok) {
38-
throw new Error(`Error fetching data: ${response.statusText}`);
39-
}
40-
41-
const data = await response.json();
42-
console.log(data);
43-
44-
// Transform data to match RecentAttempt interface
45-
const formattedData: HistoryEntry[] = data.map((entry: any) => ({
46-
key: entry._id,
47-
id: entry._id,
48-
date: new Date(entry.attemptStartedAt).toLocaleDateString(),
49-
title: entry.question.title || 'Unknown Title',
50-
difficulty: entry.question.difficulty || 'Easy',
51-
topics: entry.question.categories.map((cat: any) => cat.name) || [],
52-
}));
53-
54-
setRecentAttemptsData(formattedData);
20+
const data = await historyUseCases.getAllCategories();
21+
setRecentAttemptsData(data);
5522
} catch (error) {
5623
if (error instanceof Error) {
5724
console.error("Failed to fetch recent attempts:", error.message);
@@ -65,22 +32,9 @@ export const RecentAttemptsTable: React.FC = () => {
6532
}
6633
};
6734

68-
// Function to handle clearing all attempts
6935
const handleClearAllAttempts = async () => {
7036
try {
71-
const token = AuthClientStore.getAccessToken();
72-
const response = await fetch('http://localhost:3002/api/history/all', {
73-
method: 'DELETE',
74-
headers: {
75-
'Content-Type': 'application/json',
76-
'Authorization': `Bearer ${token}`,
77-
},
78-
});
79-
80-
if (!response.ok) {
81-
throw new Error(`Error clearing attempts: ${response.statusText}`);
82-
}
83-
37+
await historyUseCases.deleteAllUserHistories();
8438
message.success("All attempts cleared successfully");
8539
setRecentAttemptsData([]);
8640
} catch (error) {
@@ -94,31 +48,14 @@ export const RecentAttemptsTable: React.FC = () => {
9448
}
9549
};
9650

97-
// Function to handle deleting selected attempts
9851
const handleDeleteSelectedAttempts = async () => {
9952
try {
10053
if (selectedRowKeys.length === 0) {
10154
message.info("No attempts selected");
10255
return;
10356
}
104-
105-
const token = AuthClientStore.getAccessToken();
106-
const response = await fetch('http://localhost:3002/api/history', {
107-
method: 'DELETE',
108-
headers: {
109-
'Content-Type': 'application/json',
110-
'Authorization': `Bearer ${token}`,
111-
},
112-
body: JSON.stringify({ ids: selectedRowKeys }),
113-
});
114-
115-
if (!response.ok) {
116-
throw new Error(`Error deleting attempts: ${response.statusText}`);
117-
}
118-
57+
await historyUseCases.deleteSelectedUserHistories(selectedRowKeys.map((key) => key.toString()));
11958
message.success(`${selectedRowKeys.length} attempt(s) deleted successfully`);
120-
121-
// Remove deleted attempts from the state
12259
setRecentAttemptsData((prevData) =>
12360
prevData.filter((attempt) => !selectedRowKeys.includes(attempt.key))
12461
);
@@ -139,7 +76,7 @@ export const RecentAttemptsTable: React.FC = () => {
13976
title: 'Date',
14077
dataIndex: 'date',
14178
key: 'date',
142-
sorter: (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
79+
sorter: (a, b) => new Date(a.attemptCompletedAt).getTime() - new Date(b.attemptCompletedAt).getTime(),
14380
},
14481
{
14582
title: 'Title',

0 commit comments

Comments
 (0)