Skip to content

Commit 8c1892b

Browse files
authored
refactor: change leetcode questions fetch method (#71)
## Description Previously, leetcode questions are fetched using LeetCode GraphQL Endpoints, which have stopped being publicly publicised due to security concerns. As such, there might be a concern of scrapping if we were to directly query LeetCode GraphQL endpoint. In this PR, we will be using a [leetcode query dependency](https://www.npmjs.com/package/leetcode-query) provided to fetch the leetcode questions instead.
1 parent 82c89dc commit 8c1892b

File tree

8 files changed

+175
-208
lines changed

8 files changed

+175
-208
lines changed

backend-services/leetcode-backend-service/package-lock.json

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend-services/leetcode-backend-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"bottleneck": "^2.19.5",
2929
"dotenv": "^17.2.2",
3030
"fastify": "^5.6.0",
31+
"leetcode-query": "^2.0.1",
3132
"mongodb": "^6.20.0",
3233
"mongoose": "^8.18.1",
3334
"p-limit": "^7.1.1",

backend-services/leetcode-backend-service/src/leetcode/client.ts

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

backend-services/leetcode-backend-service/src/leetcode/queries.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
export const QUERY_LIST = `
2-
query problemsetQuestionList($categorySlug:String,$limit:Int,$skip:Int,$filters:QuestionListFilterInput){
3-
problemsetQuestionList: questionList(categorySlug:$categorySlug, limit:$limit, skip:$skip, filters:$filters){
4-
total: totalNum
5-
questions: data { title titleSlug difficulty isPaidOnly questionFrontendId }
6-
}
7-
}`;
8-
91
export const QUERY_DETAIL = `
102
query question($titleSlug:String!){
113
question(titleSlug:$titleSlug){

backend-services/leetcode-backend-service/src/leetcode/seedBatch.ts

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
* to continue fetching more questions until all are processed.
77
*/
88
import { Question, SeedCursor } from "../db/model/question.js";
9-
import { gql } from "./client.js";
10-
import { QUERY_LIST, QUERY_DETAIL } from "./queries.js";
11-
import type { BasicInformation, QuestionList, Details } from "./types.js";
9+
import { QUERY_DETAIL } from "./queries.js";
10+
import type { BasicInformation, Details } from "./types.js";
1211
import pLimit from "p-limit";
1312
import { logger } from "../logger.js";
1413
import { checkQuestionServiceHealth } from "../health.js";
14+
import { LeetCode } from "leetcode-query";
1515

1616
const PAGE_SIZE = 200;
1717
/**
@@ -32,6 +32,8 @@ const DIFFICULTY_TIME_LIMITS: Record<string, number> = {
3232
Hard: 120,
3333
};
3434

35+
const leetcode = new LeetCode();
36+
3537
/**
3638
* Run one batch (default pageSize=200). Returns a summary.
3739
* @returns An object containing the result of the seeding operation.
@@ -63,10 +65,16 @@ export async function seedLeetCodeBatch() {
6365
let total = 0;
6466

6567
try {
66-
const { questionList: fetchedQuestionList, total: fetchedTotal } =
67-
await fetchNonPaidQuestionList(pageSize, nextSkip);
68-
questionList = fetchedQuestionList;
69-
total = fetchedTotal;
68+
const res = await leetcode.problems({ limit: pageSize, offset: nextSkip });
69+
70+
questionList = res.questions
71+
.filter((p) => !p.isPaidOnly)
72+
.map((p) => ({
73+
titleSlug: p.titleSlug,
74+
title: p.title,
75+
isPaidOnly: p.isPaidOnly,
76+
}));
77+
total = res.total ?? res.questions.length;
7078
} catch (err) {
7179
logger.error(
7280
`Failed to fetch question list from LeetCode: ${(err as Error).message}`,
@@ -95,7 +103,6 @@ export async function seedLeetCodeBatch() {
95103

96104
const questionInfos: QuestionDetail[] =
97105
await fetchNonPaidQuestionInfo(questionList);
98-
99106
const ops = questionInfos.map((q) => ({
100107
updateOne: {
101108
filter: { titleSlug: q.titleSlug },
@@ -180,40 +187,14 @@ export async function fetchNonPaidQuestionInfo(
180187
return results.filter((d): d is QuestionDetail => d !== null);
181188
}
182189

183-
/**
184-
* Fetch non-paid question list.
185-
* We will only store non-paid questions in our database because
186-
* content of paid questions will not be accessible without a premium account.
187-
*/
188-
export async function fetchNonPaidQuestionList(
189-
limit: number,
190-
skip: number,
191-
): Promise<{
192-
questionList: BasicInformation[];
193-
total: number;
194-
}> {
195-
const res = await gql<
196-
QuestionList,
197-
{
198-
categorySlug: string;
199-
limit: number;
200-
skip: number;
201-
filters: Record<string, unknown>;
202-
}
203-
>(QUERY_LIST, { categorySlug: "", limit: limit, skip: skip, filters: {} });
190+
export async function getQuestionDetail(slug: string) {
191+
const res = (await leetcode.graphql({
192+
query: QUERY_DETAIL,
193+
variables: { titleSlug: slug },
194+
})) as { data: { question: QuestionDetail | null } | null };
204195

205-
if (!res.problemsetQuestionList) {
206-
throw new Error("Failed to fetch question list from LeetCode");
196+
if (!res || !res.data || !res.data.question) {
197+
return null;
207198
}
208-
209-
const { total, questions } = res.problemsetQuestionList;
210-
const questionList = questions.filter((q) => !q.isPaidOnly);
211-
return { questionList, total };
212-
}
213-
214-
export async function getQuestionDetail(slug: string) {
215-
const res = await gql<Details, { titleSlug: string }>(QUERY_DETAIL, {
216-
titleSlug: slug,
217-
});
218-
return res.question;
199+
return res.data.question;
219200
}

backend-services/leetcode-backend-service/src/leetcode/types.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,13 @@ export type BasicInformation = {
66
title: string;
77
titleSlug: string;
88
isPaidOnly: boolean;
9-
difficulty: "Easy" | "Medium" | "Hard";
10-
categoryTitle?: string | null;
11-
};
12-
13-
export type QuestionList = {
14-
problemsetQuestionList: {
15-
total: number;
16-
questions: BasicInformation[];
17-
};
189
};
1910

2011
export type Details = {
2112
question:
2213
| (BasicInformation & {
14+
difficulty: "Easy" | "Medium" | "Hard";
15+
categoryTitle?: string | null;
2316
content: string | null;
2417
exampleTestcases?: string | null;
2518
hints?: string[] | null;

0 commit comments

Comments
 (0)