diff --git a/src/Controllers/fetchUserDetails.ts b/src/Controllers/fetchUserDetails.ts index c1f0914..05efd0d 100644 --- a/src/Controllers/fetchUserDetails.ts +++ b/src/Controllers/fetchUserDetails.ts @@ -1,11 +1,10 @@ import { Response } from 'express'; -import { UserData } from '../types'; -const fetchUserDetails = async ( +const fetchUserDetails = async ( options: { username: string; limit: number; year: number }, res: Response, query: string, - formatData?: (data: UserData) => {}, + formatData?: (data: T) => U ) => { try { const response = await fetch('https://leetcode.com/graphql', { @@ -19,7 +18,7 @@ const fetchUserDetails = async ( variables: { username: options.username, //username required limit: options.limit, //only for submission - year: options.year + year: options.year, }, }), }); @@ -30,15 +29,14 @@ const fetchUserDetails = async ( return res.send(result); } - if(formatData == null) { + if (formatData == null) { return res.json(result.data); } - return res.json(formatData(result.data)); } catch (err) { console.error('Error: ', err); - return res.send(err); + return res.send(err.message); } }; diff --git a/src/FormatUtils/formatter.ts b/src/FormatUtils/formatter.ts new file mode 100644 index 0000000..3a891ee --- /dev/null +++ b/src/FormatUtils/formatter.ts @@ -0,0 +1,11 @@ +import { ZodType } from 'zod'; + +export const withSchema = + (schema: ZodType, formatter: (data: T) => U) => + (input: T) => { + const result = schema.safeParse(input); + if (result.success) { + return formatter(result.data); + } + throw new Error(result.error.message); + }; diff --git a/src/FormatUtils/index.ts b/src/FormatUtils/index.ts index 9004087..a6d25a8 100644 --- a/src/FormatUtils/index.ts +++ b/src/FormatUtils/index.ts @@ -1,4 +1,10 @@ +import { userContest } from '../schema'; +import { withSchema } from './formatter'; +import { formatContestData as _formatContestData } from './userData'; + export * from './userData'; export * from './problemData'; export * from './trendingTopicData'; -export * from './userProfileData'; \ No newline at end of file +export * from './userProfileData'; + +export const formatContestData = withSchema(userContest, _formatContestData); diff --git a/src/FormatUtils/userData.ts b/src/FormatUtils/userData.ts index d8717b1..7e11a1a 100644 --- a/src/FormatUtils/userData.ts +++ b/src/FormatUtils/userData.ts @@ -1,3 +1,4 @@ +import { UserContest } from '../schema'; import { UserData } from '../types'; export const formatUserData = (data: UserData) => ({ @@ -25,7 +26,7 @@ export const formatBadgesData = (data: UserData) => ({ activeBadge: data.matchedUser.activeBadge, }); -export const formatContestData = (data: UserData) => ({ +export const formatContestData = (data: UserContest) => ({ contestAttend: data.userContestRanking?.attendedContestsCount, contestRating: data.userContestRanking?.rating, contestGlobalRanking: data.userContestRanking?.globalRanking, @@ -70,15 +71,15 @@ export const formatSubmissionCalendarData = (data: UserData) => ({ }); export const formatSkillStats = (data: UserData) => ({ -fundamental: data.matchedUser.tagProblemCounts.fundamental, -intermediate: data.matchedUser.tagProblemCounts.intermediate, -advanced: data.matchedUser.tagProblemCounts.advanced, + fundamental: data.matchedUser.tagProblemCounts.fundamental, + intermediate: data.matchedUser.tagProblemCounts.intermediate, + advanced: data.matchedUser.tagProblemCounts.advanced, }); export const formatLanguageStats = (data: UserData) => ({ -languageProblemCount: data.matchedUser.languageProblemCount, + languageProblemCount: data.matchedUser.languageProblemCount, }); export const formatProgressStats = (data: UserData) => ({ -numAcceptedQuestions: data.userProfileUserQuestionProgressV2 + numAcceptedQuestions: data.userProfileUserQuestionProgressV2, }); diff --git a/src/__tests__/msw/mockData/singleUserContests.json b/src/__tests__/msw/mockData/singleUserContests.json index 5f45892..6ce1b19 100644 --- a/src/__tests__/msw/mockData/singleUserContests.json +++ b/src/__tests__/msw/mockData/singleUserContests.json @@ -9,7 +9,11 @@ "trendDirection": "NONE", "problemsSolved": 0, "totalProblems": 3, - "finishTimeInSeconds": 0 + "finishTimeInSeconds": 0, + "contest": { + "title": "Biweekly Contest 52", + "startTime": 1621089000 + } } ] } diff --git a/src/leetCode.ts b/src/leetCode.ts index d9e59b4..2b45afc 100644 --- a/src/leetCode.ts +++ b/src/leetCode.ts @@ -10,7 +10,7 @@ export const userData = (req: TransformedUserDataRequest, res: Response) => { req.body, res, gqlQueries.userProfileQuery, - formatUtils.formatUserData, + formatUtils.formatUserData ); }; @@ -19,7 +19,7 @@ export const userBadges = (req: TransformedUserDataRequest, res: Response) => { req.body, res, gqlQueries.userProfileQuery, - formatUtils.formatBadgesData, + formatUtils.formatBadgesData ); }; @@ -28,7 +28,7 @@ export const userContest = (req: TransformedUserDataRequest, res: Response) => { req.body, res, gqlQueries.contestQuery, - formatUtils.formatContestData, + formatUtils.formatContestData ); }; @@ -40,7 +40,7 @@ export const userContestHistory = ( req.body, res, gqlQueries.contestQuery, - formatUtils.formatContestHistoryData, + formatUtils.formatContestHistoryData ); }; @@ -52,7 +52,7 @@ export const solvedProblem = ( req.body, res, gqlQueries.userProfileQuery, - formatUtils.formatSolvedProblemsData, + formatUtils.formatSolvedProblemsData ); }; @@ -61,7 +61,7 @@ export const submission = (req: TransformedUserDataRequest, res: Response) => { req.body, res, gqlQueries.submissionQuery, - formatUtils.formatSubmissionData, + formatUtils.formatSubmissionData ); }; @@ -73,7 +73,7 @@ export const acSubmission = ( req.body, res, gqlQueries.AcSubmissionQuery, - formatUtils.formatAcSubmissionData, + formatUtils.formatAcSubmissionData ); }; @@ -101,7 +101,7 @@ export const userProfile = (req: Request, res: Response) => { res, gqlQueries.getUserProfileQuery, formatUtils.formatUserProfileData - ) + ); }; export const languageStats = (req: Request, res: Response) => { @@ -110,7 +110,7 @@ export const languageStats = (req: Request, res: Response) => { res, gqlQueries.languageStatsQuery, formatUtils.formatLanguageStats - ) + ); }; export const progress = (req: Request, res: Response) => { @@ -119,7 +119,7 @@ export const progress = (req: Request, res: Response) => { res, gqlQueries.userQuestionProgressQuery, formatUtils.formatProgressStats - ) + ); }; //Problems Details @@ -128,16 +128,12 @@ export const dailyProblem = (_req: Request, res: Response) => { res, gqlQueries.dailyProblemQuery, null, - formatUtils.formatDailyData, + formatUtils.formatDailyData ); }; export const dailyProblemRaw = (_req: Request, res: Response) => { - controllers.fetchSingleProblem( - res, - gqlQueries.dailyProblemQuery, - null, - ); + controllers.fetchSingleProblem(res, gqlQueries.dailyProblemQuery, null); }; export const selectProblem = (req: Request, res: Response) => { @@ -147,7 +143,7 @@ export const selectProblem = (req: Request, res: Response) => { res, gqlQueries.selectProblemQuery, title, - formatUtils.formatQuestionData, + formatUtils.formatQuestionData ); } else { res.status(400).json({ @@ -161,11 +157,7 @@ export const selectProblem = (req: Request, res: Response) => { export const selectProblemRaw = (req: Request, res: Response) => { const title = req.query.titleSlug as string; if (title !== undefined) { - controllers.fetchSingleProblem( - res, - gqlQueries.selectProblemQuery, - title, - ); + controllers.fetchSingleProblem(res, gqlQueries.selectProblemQuery, title); } else { res.status(400).json({ error: 'Missing or invalid query parameter: titleSlug', @@ -173,10 +165,15 @@ export const selectProblemRaw = (req: Request, res: Response) => { example: 'localhost:3000/select?titleSlug=two-sum', }); } -} +}; export const problems = ( - req: Request<{}, {}, {}, { limit: number; skip: number; tags: string; difficulty: string }>, + req: Request< + {}, + {}, + {}, + { limit: number; skip: number; tags: string; difficulty: string } + >, res: Response ) => { const difficulty = req.query.difficulty; @@ -198,7 +195,9 @@ export const officialSolution = (req: Request, res: Response) => { if (!titleSlug) { return res.status(400).json({ error: 'Missing titleSlug query parameter' }); } - return controllers.handleRequest(res, gqlQueries.officialSolutionQuery, { titleSlug }); + return controllers.handleRequest(res, gqlQueries.officialSolutionQuery, { + titleSlug, + }); }; // Discussion @@ -211,15 +210,13 @@ export const trendingCategoryTopics = (_req: Request, res: Response) => { formatUtils.formatTrendingCategoryTopicData, gqlQueries.trendingDiscussQuery ); - } - else { + } else { res.status(400).json({ error: 'Missing or invalid query parameter: limit', solution: 'put query after discussion', example: 'localhost:3000/trendingDiscuss?first=20', }); } - }; export const discussTopic = (req: Request, res: Response) => { @@ -242,8 +239,6 @@ export const discussComments = (req: Request, res: Response) => { }); }; - - /* ----- Migrated to new functions -> these will be deleted -----*/ export const languageStats_ = (_req: Request, res: Response) => { const username = _req.query.username as string; @@ -253,8 +248,7 @@ export const languageStats_ = (_req: Request, res: Response) => { res, gqlQueries.languageStatsQuery ); - } - else { + } else { res.status(400).json({ error: 'Missing or invalid query parameter: username', solution: 'put query after discussion', @@ -263,7 +257,6 @@ export const languageStats_ = (_req: Request, res: Response) => { } }; - export const userProfileCalendar_ = (req: Request, res: Response) => { const { username, year } = req.query; @@ -281,9 +274,14 @@ export const userProfileCalendar_ = (req: Request, res: Response) => { export const userProfile_ = (req: Request, res: Response) => { const user = req.params.id; - controllers.fetchUserProfile(res, gqlQueries.getUserProfileQuery, { - username: user, - }, formatUtils.formatUserProfileData); + controllers.fetchUserProfile( + res, + gqlQueries.getUserProfileQuery, + { + username: user, + }, + formatUtils.formatUserProfileData + ); }; export const dailyQuestion_ = (_req: Request, res: Response) => { @@ -295,33 +293,28 @@ export const skillStats_ = (req: Request, res: Response) => { controllers.handleRequest(res, gqlQueries.skillStatsQuery, { username }); }; -export const userProfileUserQuestionProgressV2_ = (req: Request, res: Response) => { +export const userProfileUserQuestionProgressV2_ = ( + req: Request, + res: Response +) => { const username = req.params.username; - controllers.handleRequest(res, gqlQueries.userQuestionProgressQuery, { username }); + controllers.handleRequest(res, gqlQueries.userQuestionProgressQuery, { + username, + }); }; export const userContestRankingInfo_ = (req: Request, res: Response) => { const { username } = req.params; - controllers.handleRequest(res, gqlQueries.userContestRankingInfoQuery, { username }); + controllers.handleRequest(res, gqlQueries.userContestRankingInfoQuery, { + username, + }); }; //limiting is not supported in the contests unlike problems -export const allContests = ( - _req: Request, - res: Response -) => { - controllers.fetchAllContests( - res, - gqlQueries.allContestQuery - ); +export const allContests = (_req: Request, res: Response) => { + controllers.fetchAllContests(res, gqlQueries.allContestQuery); }; -export const upcomingContests = ( - _req: Request, - res: Response -) => { - controllers.fetchUpcomingContests( - res, - gqlQueries.allContestQuery - ); +export const upcomingContests = (_req: Request, res: Response) => { + controllers.fetchUpcomingContests(res, gqlQueries.allContestQuery); }; diff --git a/src/schema/common/index.ts b/src/schema/common/index.ts new file mode 100644 index 0000000..7824fa6 --- /dev/null +++ b/src/schema/common/index.ts @@ -0,0 +1,6 @@ +import z from 'zod'; + +export const badge = z.object({ + name: z.string(), + icon: z.string().optional(), +}); diff --git a/src/schema/contests.ts b/src/schema/contests.ts new file mode 100644 index 0000000..21a49dd --- /dev/null +++ b/src/schema/contests.ts @@ -0,0 +1,33 @@ +import z from 'zod'; +import { badge } from './common'; + +const userContestRanking = z.object({ + attendedContestsCount: z.number().nonnegative(), + badge: badge.nullable(), + globalRanking: z.number().nonnegative(), + rating: z.number().nonnegative(), + totalParticipants: z.number().nonnegative(), + topPercentage: z.number().nonnegative(), +}); + +const userContestRankingHistory = z.object({ + attended: z.boolean(), + rating: z.number().nonnegative(), + ranking: z.number().nonnegative(), + trendDirection: z.string(), + problemsSolved: z.number().nonnegative(), + totalProblems: z.number().positive(), + finishTimeInSeconds: z.number().nonnegative(), + contest: z.object({ + title: z.string(), + startTime: z.number(), + }), +}); + +const userContest = z.object({ + userContestRanking: userContestRanking.nullable(), + userContestRankingHistory: z.array(userContestRankingHistory), +}); + +export default userContest; +export type UserContest = z.infer; diff --git a/src/schema/index.ts b/src/schema/index.ts new file mode 100644 index 0000000..0a5202a --- /dev/null +++ b/src/schema/index.ts @@ -0,0 +1 @@ +export { default as userContest, UserContest } from './contests';