-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: add draft for data module #28
Changes from 37 commits
c5210c0
1cf4c52
56e239f
3f79d97
02b602b
31bbf09
feaaa8f
38021c1
d8f96be
9f163b7
6fbfe7a
e6590c1
d4639a7
2378ee2
0c95ee6
7069566
ed91ab0
f13112a
8b0ba70
ee0c9cc
86bb7ca
f5dec9c
ddb16ec
65f05cc
204e9de
2ffaef9
6a21ba6
fda3341
9a43193
363ff4a
969f60d
0865793
4230f14
b8322bf
cf784b1
bfeba2d
a6c9946
1798d60
8b50286
d2ee200
d405e1b
e4af42b
ef78a24
21ee78c
dfe5b0c
044e63b
d140a4d
c31a5b5
c33c3cd
9f8b2d0
212435d
86e9e2c
24866ca
b7f5ab4
dd84127
b47f411
9cc85bf
0c2962d
7d577ff
6c94c93
f36372b
4922cc5
3837225
b1eb0ed
b561ba1
db3e480
a163cb3
4946eaf
09a8ba8
38d492f
90197a9
747d927
ec2c708
2d66dc7
4f57007
bb3dc47
f7c7eb1
453464f
6825e8d
1f3f11c
d814bca
0d0747d
b3ab1d4
98ff3cc
7e8b9d2
a25315f
1abdff9
2b9d62d
acbb811
3531a92
606ef44
e2249dd
a2252ed
0577a41
6edad94
8ac9720
72f710b
842aa44
a37e7a1
d295ca2
e78f71a
ce89bd5
c28a759
0370bd0
fc49ddf
8f8c5a3
265f97b
8305e9b
57e9ff3
2ae8ade
77ac38d
e39bbde
2048e99
fadf497
f4806d6
049de21
3319125
2498290
dd44373
b298f45
1c4aae0
6f70aa0
7bbd017
ff22c69
618236a
b7ef969
dafdbfd
31e28ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import type { Grade } from "../type"; | ||
import type { Config } from "./type"; | ||
|
||
export const CONFIG: Config = { | ||
study: { | ||
organization: "DaleStudy", | ||
repository: "leetcode-study", | ||
branchName: "main", | ||
teamPrefix: "leetcode", | ||
totalProblemCount: 75, | ||
gradeThresholds: [ | ||
["BIG_TREE", 70], | ||
["SMALL_TREE", 60], | ||
["SPROUT", 50], | ||
["SEED", 0], | ||
] as [Grade, number][], | ||
}, | ||
gitHub: { | ||
baseUrl: "https://api.github.com", | ||
mediaType: "application/vnd.github+json", | ||
token: process.env.GITHUB_TOKEN ?? "", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 브라우저 환경에서는 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분이 계속 고민입니다 지금 그나마 생각나는 방법은
oauth 등의 방법도 팀원이 아니라면 팀원 목록을 볼 수 없는 등 문제가 있어서 결국 팀에 속한 계정의 토큰이 필요할것 같습니다. 그게 아니라면 팀원 목록을 관리할 외부 소스를 두는것도 방법일수 있을 것 같습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
}, | ||
} as const; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,21 @@ | ||||||
import { Grade } from "../type"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
export type StudyConfig = { | ||||||
organization: string; | ||||||
repository: string; | ||||||
branchName: string; | ||||||
teamPrefix: string; | ||||||
totalProblemCount: number; | ||||||
gradeThresholds: [Grade, number][]; | ||||||
}; | ||||||
|
||||||
export type GitHubConfig = { | ||||||
baseUrl: string; | ||||||
mediaType: string; | ||||||
token: string; | ||||||
}; | ||||||
|
||||||
export type Config = { | ||||||
study: StudyConfig; | ||||||
gitHub: GitHubConfig; | ||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { CONFIG } from "./config"; | ||
import { createStoreService } from "./services/store/storeService"; | ||
|
||
export const leaderBoardStore = await createStoreService(CONFIG); | ||
|
||
console.log(await leaderBoardStore.getData()); | ||
console.log(await leaderBoardStore.getMemberById("hc")); | ||
console.log(await leaderBoardStore.getMemberByCohort(1)); | ||
console.log(await leaderBoardStore.getMemberByGrade("SEED")); | ||
DaleSeo marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import type { GitHubTeam, GitHubMember, GitHubTree } from "./types"; | ||
|
||
export const mockGitHubTeams: GitHubTeam[] = [ | ||
{ | ||
name: "leetcode01", | ||
id: 1, | ||
node_id: "T", | ||
slug: "leetcode01", | ||
description: "리트코드 스터디", | ||
privacy: "closed", | ||
notification_setting: "notifications_enabled", | ||
url: "some-url", | ||
html_url: "some-url", | ||
members_url: "members-url", | ||
repositories_url: "repositories-url", | ||
permission: "pull", | ||
parent: null, | ||
}, | ||
{ | ||
name: "leetcode02", | ||
id: 2, | ||
node_id: "D", | ||
slug: "leetcode", | ||
description: "리트코드 스터디", | ||
privacy: "closed", | ||
notification_setting: "notifications_enabled", | ||
url: "some-url", | ||
html_url: "some-url", | ||
members_url: "members-url", | ||
repositories_url: "repositories-url", | ||
permission: "pull", | ||
parent: null, | ||
}, | ||
]; | ||
|
||
export const mockGitHubMembers: GitHubMember[] = [ | ||
{ | ||
login: "D", | ||
id: 1, | ||
node_id: "M", | ||
avatar_url: "avatar-url", | ||
gravatar_id: "", | ||
url: "some-url", | ||
html_url: "some-url", | ||
followers_url: "some-url", | ||
following_url: "some-url", | ||
gists_url: "some-url", | ||
starred_url: "some-url", | ||
subscriptions_url: "some-url", | ||
organizations_url: "some-url", | ||
repos_url: "some-url", | ||
events_url: "some-url", | ||
received_events_url: "some-url", | ||
type: "User", | ||
user_view_type: "public", | ||
site_admin: false, | ||
}, | ||
{ | ||
login: "S", | ||
id: 2, | ||
node_id: "E", | ||
avatar_url: "avatar-url", | ||
gravatar_id: "", | ||
url: "some-url", | ||
html_url: "some-url", | ||
followers_url: "some-url", | ||
following_url: "some-url", | ||
gists_url: "some-url", | ||
starred_url: "some-url", | ||
subscriptions_url: "some-url", | ||
organizations_url: "some-url", | ||
repos_url: "some-url", | ||
events_url: "some-url", | ||
received_events_url: "some-url", | ||
type: "User", | ||
user_view_type: "public", | ||
site_admin: false, | ||
}, | ||
]; | ||
|
||
export const mockGitHubTree: GitHubTree[] = [ | ||
{ | ||
path: "best-time-to-buy-and-sell-stock/test.java", | ||
type: "blob", | ||
mode: "100644", | ||
sha: "1", | ||
size: 0, | ||
url: "some-url", | ||
}, | ||
{ | ||
path: "best-time-to-buy-and-sell-stock/test2.java", | ||
type: "blob", | ||
mode: "100644", | ||
sha: "2", | ||
size: 0, | ||
url: "some-url", | ||
}, | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { test, expect, beforeEach, vi } from "vitest"; | ||
import { createGitHubClient } from "./gitHubClient"; | ||
import { CONFIG } from "../../config"; | ||
import { mockGitHubTeams, mockGitHubMembers, mockGitHubTree } from "./fixtures"; | ||
|
||
const mockConfig = { | ||
...CONFIG.gitHub, | ||
token: "test-token", | ||
}; | ||
|
||
const mockFetch = vi.fn(); | ||
global.fetch = mockFetch; | ||
|
||
beforeEach(() => { | ||
mockFetch.mockClear(); | ||
}); | ||
|
||
test("getTeamNames should fetch and return team names", async () => { | ||
// Arrange | ||
mockFetch.mockResolvedValue({ | ||
ok: true, | ||
json: () => Promise.resolve(mockGitHubTeams), | ||
}); | ||
const client = createGitHubClient(mockConfig); | ||
const expectedUrl = `${mockConfig.baseUrl}/orgs/test-org/teams`; | ||
const expectedHeaders = { | ||
Accept: mockConfig.mediaType, | ||
Authorization: `token ${mockConfig.token}`, | ||
}; | ||
|
||
// Act | ||
const result = await client.getTeamNames("test-org"); | ||
|
||
// Assert | ||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, { | ||
headers: expectedHeaders, | ||
}); | ||
expect(result).toEqual(["leetcode01", "leetcode02"]); | ||
}); | ||
|
||
test("getTeamNames should throw error when fetch fails", async () => { | ||
// Arrange | ||
mockFetch.mockResolvedValue({ | ||
ok: false, | ||
status: 404, | ||
statusText: "Not Found", | ||
}); | ||
const client = createGitHubClient(mockConfig); | ||
|
||
// Act & Assert | ||
await expect(client.getTeamNames("test-org")).rejects.toThrow( | ||
"Failed to fetch url", | ||
); | ||
}); | ||
|
||
test("getTeamMembers should fetch and return team members", async () => { | ||
// Arrange | ||
mockFetch.mockResolvedValue({ | ||
ok: true, | ||
json: () => Promise.resolve(mockGitHubMembers), | ||
}); | ||
const client = createGitHubClient(mockConfig); | ||
const expectedUrl = `${mockConfig.baseUrl}/orgs/test-org/teams/test-team/members`; | ||
const expectedHeaders = { | ||
Accept: mockConfig.mediaType, | ||
Authorization: `token ${mockConfig.token}`, | ||
}; | ||
|
||
// Act | ||
const result = await client.getTeamMembers("test-org", "test-team"); | ||
|
||
// Assert | ||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, { | ||
headers: expectedHeaders, | ||
}); | ||
expect(result).toEqual(mockGitHubMembers); | ||
}); | ||
|
||
test("getDirectoryTree should fetch and return directory tree", async () => { | ||
// Arrange | ||
mockFetch.mockResolvedValue({ | ||
ok: true, | ||
json: () => Promise.resolve({ tree: mockGitHubTree }), | ||
}); | ||
const client = createGitHubClient(mockConfig); | ||
const expectedUrl = `${mockConfig.baseUrl}/repos/test-owner/test-repo/git/trees/main?recursive=1`; | ||
const expectedHeaders = { | ||
Accept: mockConfig.mediaType, | ||
Authorization: `token ${mockConfig.token}`, | ||
}; | ||
|
||
// Act | ||
const result = await client.getDirectoryTree( | ||
"test-owner", | ||
"test-repo", | ||
"main", | ||
); | ||
|
||
// Assert | ||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, { | ||
headers: expectedHeaders, | ||
}); | ||
expect(result).toEqual(mockGitHubTree); | ||
}); | ||
|
||
test("error should include detailed information", async () => { | ||
// Arrange | ||
const status = 403; | ||
const statusText = "Forbidden"; | ||
mockFetch.mockResolvedValue({ | ||
ok: false, | ||
status, | ||
statusText, | ||
}); | ||
const client = createGitHubClient(mockConfig); | ||
const expectedErrorMessage = `Failed to fetch url: ${mockConfig.baseUrl}/orgs/test-org/teams, status: ${status}, statusText: ${statusText}`; | ||
|
||
// Act & Assert | ||
await expect(client.getTeamNames("test-org")).rejects.toThrow( | ||
expectedErrorMessage, | ||
); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import type { GitHubConfig } from "../../config/type"; | ||
import type { | ||
GitHubMember, | ||
GitHubTeam, | ||
GitHubTree, | ||
GitHubTreeResponse, | ||
} from "./types"; | ||
|
||
export const createGitHubClient = (config: GitHubConfig) => { | ||
const request = async (url: string, token: string): Promise<unknown> => { | ||
const response = await fetch(url, { | ||
headers: { | ||
Accept: config.mediaType, | ||
Authorization: `token ${token}`, | ||
}, | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new Error( | ||
`Failed to fetch url: ${url}, status: ${response.status}, statusText: ${response.statusText}`, | ||
); | ||
} | ||
|
||
return response.json(); | ||
}; | ||
|
||
return { | ||
getTeamNames: async (organization: string): Promise<string[]> => | ||
request( | ||
`${config.baseUrl}/orgs/${organization}/teams`, | ||
config.token, | ||
).then((response) => (response as GitHubTeam[]).map((team) => team.name)), | ||
|
||
getTeamMembers: async ( | ||
organization: string, | ||
teamName: string, | ||
): Promise<GitHubMember[]> => | ||
request( | ||
`${config.baseUrl}/orgs/${organization}/teams/${teamName}/members`, | ||
config.token, | ||
).then((response) => response as GitHubMember[]), | ||
|
||
getDirectoryTree: async ( | ||
owner: string, | ||
repo: string, | ||
treeSha: string, | ||
recursive = 1, | ||
): Promise<GitHubTree[]> => | ||
request( | ||
`${config.baseUrl}/repos/${owner}/${repo}/git/trees/${treeSha}?recursive=${recursive}`, | ||
config.token, | ||
).then((response) => (response as GitHubTreeResponse).tree), | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
export type GitHubTeam = { | ||
name: string; | ||
id: number; | ||
node_id: string; | ||
slug: string; | ||
description: string; | ||
privacy: string; | ||
notification_setting: string; | ||
url: string; | ||
html_url: string; | ||
members_url: string; | ||
repositories_url: string; | ||
permission: string; | ||
parent: null; | ||
}; | ||
|
||
export type GitHubMember = { | ||
login: string; | ||
id: number; | ||
node_id: string; | ||
avatar_url: string; | ||
gravatar_id: string; | ||
url: string; | ||
html_url: string; | ||
followers_url: string; | ||
following_url: string; | ||
gists_url: string; | ||
starred_url: string; | ||
subscriptions_url: string; | ||
organizations_url: string; | ||
repos_url: string; | ||
events_url: string; | ||
received_events_url: string; | ||
type: string; | ||
user_view_type: string; | ||
site_admin: boolean; | ||
}; | ||
|
||
export type GitHubTreeResponse = { | ||
sha: string; | ||
url: string; | ||
tree: GitHubTree[]; | ||
}; | ||
|
||
export type GitHubTree = { | ||
path: string; | ||
mode: string; | ||
type: string; | ||
size: number; | ||
sha: string; | ||
url: string; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요 거는 바뀔 가능성이 거의 0%라 설정으로 뽑지 않았으면 합니다. 설사 바뀔 일이 생기더라도 쓰이는 곳이 딱 한 군데라서 아주 쉽게 코드 수정을 할 수 있을 것 같아요.