Skip to content

Commit dc2c2ab

Browse files
2 parents bb21193 + f2d73ea commit dc2c2ab

89 files changed

Lines changed: 2190 additions & 2011 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/pull_request_template.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
### Checks
66

7-
- [ ] Adding/modifying Typescript code?
8-
- [ ] I have used `qs`, `qsa` or `qsr` instead of JQuery selectors.
97
- [ ] Adding quotes?
108
- [ ] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content.
119
- [ ] Adding a language?

backend/__tests__/__integration__/dal/user.spec.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { describe, it, expect, vi } from "vitest";
1+
import { describe, expect, it, vi } from "vitest";
22

3+
import { CustomThemeColors } from "@monkeytype/schemas/configs";
4+
import { PersonalBest, PersonalBests } from "@monkeytype/schemas/shared";
5+
import { MonkeyMail, ResultFilters } from "@monkeytype/schemas/users";
6+
import { ObjectId } from "mongodb";
37
import * as UserDAL from "../../../src/dal/user";
4-
import * as UserTestData from "../../__testData__/users";
58
import { createConnection as createFriend } from "../../__testData__/connections";
6-
import { ObjectId } from "mongodb";
7-
import { MonkeyMail, ResultFilters } from "@monkeytype/schemas/users";
8-
import { PersonalBest, PersonalBests } from "@monkeytype/schemas/shared";
9-
import { CustomThemeColors } from "@monkeytype/schemas/configs";
9+
import * as UserTestData from "../../__testData__/users";
1010

1111
const mockPersonalBest: PersonalBest = {
1212
acc: 1,
@@ -1122,6 +1122,55 @@ describe("UserDal", () => {
11221122
expect(year2024[93]).toEqual(2);
11231123
});
11241124
});
1125+
1126+
describe("getUser", () => {
1127+
it("should get with missing personalBests", async () => {
1128+
//GIVEN
1129+
let user = await UserTestData.createUser({ personalBests: undefined });
1130+
1131+
//WHEN
1132+
const read = await UserDAL.getUser(user.uid, "read");
1133+
1134+
expect(read.personalBests).toEqual({
1135+
custom: {},
1136+
quote: {},
1137+
time: {},
1138+
words: {},
1139+
zen: {},
1140+
});
1141+
});
1142+
});
1143+
1144+
describe("getUserByName", () => {
1145+
it("should get with missing personalBests", async () => {
1146+
//GIVEN
1147+
let user = await UserTestData.createUser({ personalBests: undefined });
1148+
1149+
//WHEN
1150+
const read = await UserDAL.getUserByName(user.name, "read");
1151+
1152+
expect(read.personalBests).toEqual({
1153+
custom: {},
1154+
quote: {},
1155+
time: {},
1156+
words: {},
1157+
zen: {},
1158+
});
1159+
});
1160+
});
1161+
1162+
describe("getPersonalBests", () => {
1163+
it("should get with missing personalBests", async () => {
1164+
//GIVEN
1165+
let user = await UserTestData.createUser({ personalBests: undefined });
1166+
1167+
//WHEN
1168+
const read = await UserDAL.getPersonalBests(user.uid, "time", "15");
1169+
1170+
expect(read).toBeUndefined();
1171+
});
1172+
});
1173+
11251174
describe("getPartialUser", () => {
11261175
it("should throw for unknown user", async () => {
11271176
await expect(async () =>
@@ -1156,6 +1205,24 @@ describe("UserDal", () => {
11561205
},
11571206
});
11581207
});
1208+
it("should get with missing personalBests", async () => {
1209+
//GIVEN
1210+
let user = await UserTestData.createUser({ personalBests: undefined });
1211+
1212+
//WHEN
1213+
const read = await UserDAL.getPartialUser(user.uid, "read", [
1214+
"uid",
1215+
"personalBests",
1216+
]);
1217+
1218+
expect(read.personalBests).toEqual({
1219+
custom: {},
1220+
quote: {},
1221+
time: {},
1222+
words: {},
1223+
zen: {},
1224+
});
1225+
});
11591226
});
11601227
describe("updateEmail", () => {
11611228
it("throws for nonexisting user", async () => {

backend/src/api/controllers/user.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import MonkeyError, {
44
isFirebaseError,
55
} from "../../utils/error";
66
import { MonkeyResponse } from "../../utils/monkey-response";
7+
import { getCached, setCached, invalidateUserCache } from "../../utils/cache";
78
import * as DiscordUtils from "../../utils/discord";
89
import {
910
buildAgentLog,
@@ -914,6 +915,10 @@ export async function getProfile(
914915
): Promise<GetProfileResponse> {
915916
const { uidOrName } = req.params;
916917

918+
const cacheKey = `user:profile:${uidOrName}`;
919+
const cached = await getCached<GetProfileResponse>(cacheKey);
920+
if (cached !== null) return cached;
921+
917922
const user = req.query.isUid
918923
? await UserDAL.getUser(uidOrName, "get user profile")
919924
: await UserDAL.getUserByName(uidOrName, "get user profile");
@@ -987,7 +992,11 @@ export async function getProfile(
987992
};
988993

989994
if (banned) {
990-
return new MonkeyResponse("Profile retrived: banned user", baseProfile);
995+
await setCached(
996+
cacheKey,
997+
new MonkeyResponse("Profile retrieved: banned user", baseProfile),
998+
);
999+
return new MonkeyResponse("Profile retrieved: banned user", baseProfile);
9911000
}
9921001

9931002
const allTimeLbs = await getAllTimeLbs(user.uid);
@@ -1005,6 +1014,10 @@ export async function getProfile(
10051014
} else {
10061015
delete profileData.testActivity;
10071016
}
1017+
await setCached(
1018+
cacheKey,
1019+
new MonkeyResponse("Profile retrieved", profileData),
1020+
);
10081021
return new MonkeyResponse("Profile retrieved", profileData);
10091022
}
10101023

@@ -1050,6 +1063,7 @@ export async function updateProfile(
10501063
};
10511064

10521065
await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory);
1066+
await invalidateUserCache(uid);
10531067

10541068
return new MonkeyResponse("Profile updated", profileDetailsUpdates);
10551069
}

backend/src/dal/user.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ import {
2727
CountByYearAndDay,
2828
Friend,
2929
} from "@monkeytype/schemas/users";
30-
import { Mode, Mode2, PersonalBest } from "@monkeytype/schemas/shared";
30+
import {
31+
Mode,
32+
Mode2,
33+
PersonalBest,
34+
PersonalBests,
35+
} from "@monkeytype/schemas/shared";
3136
import { addImportantLog } from "./logs";
3237
import { Result as ResultType } from "@monkeytype/schemas/results";
3338
import { Configuration } from "@monkeytype/schemas/configuration";
@@ -246,7 +251,7 @@ export async function updateEmail(
246251
export async function getUser(uid: string, stack: string): Promise<DBUser> {
247252
const user = await getUsersCollection().findOne({ uid });
248253
if (!user) throw new MonkeyError(404, "User not found", stack);
249-
return user;
254+
return migrateUser(user);
250255
}
251256

252257
/**
@@ -263,10 +268,16 @@ export async function getPartialUser<K extends keyof DBUser>(
263268
fields: K[],
264269
): Promise<Pick<DBUser, K>> {
265270
const projection = new Map(fields.map((it) => [it, 1]));
266-
const results = await getUsersCollection().findOne({ uid }, { projection });
267-
if (results === null) throw new MonkeyError(404, "User not found", stack);
271+
const partialUser = await getUsersCollection().findOne(
272+
{ uid },
273+
{ projection },
274+
);
275+
if (partialUser === null) throw new MonkeyError(404, "User not found", stack);
268276

269-
return results;
277+
if (fields.includes("personalBests" as K)) {
278+
return migrateUser(partialUser);
279+
}
280+
return partialUser;
270281
}
271282

272283
export async function findByName(name: string): Promise<DBUser | undefined> {
@@ -294,7 +305,7 @@ export async function getUserByName(
294305
): Promise<DBUser> {
295306
const user = await findByName(name);
296307
if (!user) throw new MonkeyError(404, "User not found", stack);
297-
return user;
308+
return migrateUser(user);
298309
}
299310

300311
export async function isDiscordIdAvailable(
@@ -1359,3 +1370,15 @@ export async function getFriends(uid: string): Promise<DBFriend[]> {
13591370
],
13601371
);
13611372
}
1373+
1374+
function migrateUser<T extends { personalBests: PersonalBests }>(user: T): T {
1375+
user.personalBests ??= {
1376+
time: {},
1377+
words: {},
1378+
quote: {},
1379+
zen: {},
1380+
custom: {},
1381+
};
1382+
1383+
return user;
1384+
}

backend/src/utils/cache.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { getConnection } from "../init/redis";
2+
3+
const CACHE_PREFIX = "cache:";
4+
const TTL = 300; // == 5 minutes
5+
6+
export async function getCached<T>(key: string): Promise<T | null> {
7+
const redis = getConnection();
8+
if (!redis) return null;
9+
10+
try {
11+
const data = await redis.get(`${CACHE_PREFIX}${key}`);
12+
if (data === null || data === undefined || data === "") return null;
13+
return JSON.parse(data) as T;
14+
} catch {
15+
return null;
16+
}
17+
}
18+
19+
export async function setCached<T>(key: string, data: T): Promise<void> {
20+
const redis = getConnection();
21+
if (!redis) return;
22+
23+
try {
24+
await redis.setex(`${CACHE_PREFIX}${key}`, TTL, JSON.stringify(data));
25+
} catch (err) {
26+
console.error("Cache set failed:", err);
27+
}
28+
}
29+
30+
export async function invalidateUserCache(userId: string): Promise<void> {
31+
const redis = getConnection();
32+
if (!redis) return;
33+
34+
try {
35+
const keys = await redis.keys(`${CACHE_PREFIX}user:profile:${userId}*`);
36+
if (keys.length > 0) {
37+
await redis.del(keys);
38+
}
39+
} catch (err) {
40+
console.error("Cache invalidation failed:", err);
41+
}
42+
}

docs/CONTRIBUTING_ADVANCED.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,6 @@ If you are on a UNIX system and you get a spawn error, run npm with `sudo`.
150150

151151
Code formatting is enforced by [Prettier](https://prettier.io/docs/en/install.html), which automatically runs every time you make a commit.
152152

153-
We are currently in the process of converting from JQuery to vanilla JS. When submitting new code, please use the `qs`, `qsa` and `qsr` helper functions. These return a class with a lot of JQuery-like methods. You can read how they work and import them from `frontend/src/ts/utils/dom.ts`.
154-
155153
For guidelines on commit messages, adding themes, languages, or quotes, please refer to [CONTRIBUTING.md](./CONTRIBUTING.md). Following these guidelines will increase the chances of getting your change accepted.
156154

157155
## Questions

frontend/__tests__/__harness__/setup-jquery.ts

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

frontend/__tests__/test/layout-emulator.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,11 @@ describe("LayoutEmulator", () => {
1313
updateAltGrState(event);
1414
});
1515

16-
const createEvent = (
17-
code: string,
18-
type: string,
19-
): JQuery.KeyboardEventBase =>
16+
const createEvent = (code: string, type: string): KeyboardEvent =>
2017
({
2118
code,
2219
type,
23-
}) as JQuery.KeyboardEventBase;
20+
}) as KeyboardEvent;
2421

2522
it("should set isAltGrPressed to true on AltRight keydown", () => {
2623
const event = createEvent("AltRight", "keydown");

frontend/__tests__/utils/dom.jsdom-spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ describe("dom", () => {
2626
handler({
2727
target: e.target,
2828
childTarget: e.childTarget,
29-
//@ts-expect-error will be added later, check TODO on the ChildEvent
3029
currentTarget: e.currentTarget,
3130
}),
3231
);
@@ -130,10 +129,6 @@ describe("dom", () => {
130129
await userEvent.click(clickTarget);
131130

132131
//THEN
133-
134-
//This is the same behavior as jQuery `.on` with selector.
135-
//The handler will be called two times,
136-
//It does NOT call on the <section> or the parent element itself
137132
expect(handler).toHaveBeenCalledTimes(2);
138133

139134
//First call is for childTarget inner2 (grand child of parent)

0 commit comments

Comments
 (0)