11import { Configuration } from "@monkeytype/contracts/schemas/configuration" ;
22import * as RedisClient from "../init/redis" ;
33import LaterQueue from "../queues/later-queue" ;
4- import { XpLeaderboardEntry } from "@monkeytype/contracts/schemas/leaderboards" ;
4+ import {
5+ RedisXpLeaderboardEntry ,
6+ RedisXpLeaderboardEntrySchema ,
7+ RedisXpLeaderboardScore ,
8+ XpLeaderboardEntry ,
9+ } from "@monkeytype/contracts/schemas/leaderboards" ;
510import { getCurrentWeekTimestamp } from "@monkeytype/util/date-and-time" ;
611import MonkeyError from "../utils/error" ;
712import { omit } from "lodash" ;
13+ import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json" ;
814
915type AddResultOpts = {
10- entry : Pick <
11- XpLeaderboardEntry ,
12- | "uid"
13- | "name"
14- | "discordId"
15- | "discordAvatar"
16- | "badgeId"
17- | "lastActivityTimestamp"
18- | "isPremium"
19- > ;
20- xpGained : number ;
21- timeTypedSeconds : number ;
16+ entry : RedisXpLeaderboardEntry ;
17+ xpGained : RedisXpLeaderboardScore ;
2218} ;
2319
2420const weeklyXpLeaderboardLeaderboardNamespace =
@@ -59,7 +55,7 @@ export class WeeklyXpLeaderboard {
5955 weeklyXpLeaderboardConfig : Configuration [ "leaderboards" ] [ "weeklyXp" ] ,
6056 opts : AddResultOpts
6157 ) : Promise < number > {
62- const { entry, xpGained, timeTypedSeconds } = opts ;
58+ const { entry, xpGained } = opts ;
6359
6460 const connection = RedisClient . getConnection ( ) ;
6561 if ( ! connection || ! weeklyXpLeaderboardConfig . enabled ) {
@@ -89,25 +85,28 @@ export class WeeklyXpLeaderboard {
8985
9086 const currentEntryTimeTypedSeconds =
9187 currentEntry !== null
92- ? ( JSON . parse ( currentEntry ) as { timeTypedSeconds : number | undefined } )
88+ ? parseJsonWithSchema ( currentEntry , RedisXpLeaderboardEntrySchema )
9389 ?. timeTypedSeconds
9490 : undefined ;
9591
9692 const totalTimeTypedSeconds =
97- timeTypedSeconds + ( currentEntryTimeTypedSeconds ?? 0 ) ;
93+ entry . timeTypedSeconds + ( currentEntryTimeTypedSeconds ?? 0 ) ;
9894
9995 const [ rank ] = await Promise . all ( [
100- // @ts -expect-error we are doing some weird file to function mapping, thats why its any
101- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
10296 connection . addResultIncrement (
10397 2 ,
10498 weeklyXpLeaderboardScoresKey ,
10599 weeklyXpLeaderboardResultsKey ,
106100 weeklyXpLeaderboardExpirationTimeInSeconds ,
107101 entry . uid ,
108102 xpGained ,
109- JSON . stringify ( { ...entry , timeTypedSeconds : totalTimeTypedSeconds } )
110- ) as Promise < number > ,
103+ JSON . stringify (
104+ RedisXpLeaderboardEntrySchema . parse ( {
105+ ...entry ,
106+ timeTypedSeconds : totalTimeTypedSeconds ,
107+ } )
108+ )
109+ ) ,
111110 LaterQueue . scheduleForNextWeek (
112111 "weekly-xp-leaderboard-results" ,
113112 "weekly-xp"
@@ -138,10 +137,8 @@ export class WeeklyXpLeaderboard {
138137 const { weeklyXpLeaderboardScoresKey, weeklyXpLeaderboardResultsKey } =
139138 this . getThisWeeksXpLeaderboardKeys ( ) ;
140139
141- // @ts -expect-error we are doing some weird file to function mapping, thats why its any
142- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
143140 const [ results , scores ] = ( await connection . getResults (
144- 2 , // How many of the arguments are redis keys (https://redis.io/docs/manual/programmability/lua-api/)
141+ 2 ,
145142 weeklyXpLeaderboardScoresKey ,
146143 weeklyXpLeaderboardResultsKey ,
147144 minRank ,
@@ -163,14 +160,32 @@ export class WeeklyXpLeaderboard {
163160
164161 const resultsWithRanks : XpLeaderboardEntry [ ] = results . map (
165162 ( resultJSON : string , index : number ) => {
166- //TODO parse with zod?
167- const parsed = JSON . parse ( resultJSON ) as XpLeaderboardEntry ;
168-
169- return {
170- ...parsed ,
171- rank : minRank + index + 1 ,
172- totalXp : parseInt ( scores [ index ] as string , 10 ) ,
173- } ;
163+ try {
164+ const parsed = parseJsonWithSchema (
165+ resultJSON ,
166+ RedisXpLeaderboardEntrySchema
167+ ) ;
168+ const scoreValue = scores [ index ] ;
169+
170+ if ( typeof scoreValue !== "string" ) {
171+ throw new Error (
172+ `Invalid score value at index ${ index } : ${ scoreValue } `
173+ ) ;
174+ }
175+
176+ return {
177+ ...parsed ,
178+ rank : minRank + index + 1 ,
179+ totalXp : parseInt ( scoreValue , 10 ) ,
180+ } ;
181+ } catch ( error ) {
182+ throw new MonkeyError (
183+ 500 ,
184+ `Failed to parse leaderboard entry at index ${ index } : ${
185+ error instanceof Error ? error . message : String ( error )
186+ } `
187+ ) ;
188+ }
174189 }
175190 ) ;
176191
@@ -187,15 +202,12 @@ export class WeeklyXpLeaderboard {
187202 ) : Promise < XpLeaderboardEntry | null > {
188203 const connection = RedisClient . getConnection ( ) ;
189204 if ( ! connection || ! weeklyXpLeaderboardConfig . enabled ) {
190- throw new MonkeyError ( 500 , "Redis connnection is unavailable" ) ;
205+ throw new MonkeyError ( 500 , "Redis connection is unavailable" ) ;
191206 }
192207
193208 const { weeklyXpLeaderboardScoresKey, weeklyXpLeaderboardResultsKey } =
194209 this . getThisWeeksXpLeaderboardKeys ( ) ;
195210
196- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
197- connection . set ;
198-
199211 const [ [ , rank ] , [ , totalXp ] , [ , _count ] , [ , result ] ] = ( await connection
200212 . multi ( )
201213 . zrevrank ( weeklyXpLeaderboardScoresKey , uid )
@@ -213,11 +225,21 @@ export class WeeklyXpLeaderboard {
213225 return null ;
214226 }
215227
216- //TODO parse with zod?
217- const parsed = JSON . parse ( ( result as string ) ?? "null" ) as Omit <
218- XpLeaderboardEntry ,
219- "rank" | "count" | "totalXp"
220- > ;
228+ // safely parse the result with error handling
229+ let parsed : RedisXpLeaderboardEntry ;
230+ try {
231+ parsed = parseJsonWithSchema (
232+ result ?? "null" ,
233+ RedisXpLeaderboardEntrySchema
234+ ) ;
235+ } catch ( error ) {
236+ throw new MonkeyError (
237+ 500 ,
238+ `Failed to parse leaderboard entry: ${
239+ error instanceof Error ? error . message : String ( error )
240+ } `
241+ ) ;
242+ }
221243
222244 return {
223245 ...parsed ,
@@ -261,8 +283,6 @@ export async function purgeUserFromXpLeaderboards(
261283 return ;
262284 }
263285
264- // @ts -expect-error we are doing some weird file to function mapping, thats why its any
265- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
266286 await connection . purgeResults (
267287 0 ,
268288 uid ,
0 commit comments