From cb9b077fca7473faa88b507f37bbf2a62a2a2ff4 Mon Sep 17 00:00:00 2001
From: Kalle Fagerberg
Date: Fri, 12 Aug 2022 17:08:09 +0200
Subject: [PATCH 01/33] Added GITHUB_API env var
---
index.ts | 3 ++-
src/github_api_client.ts | 7 +++++--
src/utils.ts | 1 +
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/index.ts b/index.ts
index a1ab91a8..9dd46fef 100644
--- a/index.ts
+++ b/index.ts
@@ -5,7 +5,8 @@ import { COLORS, Theme } from "./src/theme.ts";
import { Error400, Error404 } from "./src/error_page.ts";
import "https://deno.land/x/dotenv@v0.5.0/load.ts";
-const client = new GithubAPIClient();
+const apiEndpoint = Deno.env.get("GITHUB_API") || CONSTANTS.DEFAULT_GITHUB_API;
+const client = new GithubAPIClient(apiEndpoint);
export default async (req: Request) => {
const params = parseParams(req);
diff --git a/src/github_api_client.ts b/src/github_api_client.ts
index 4ca5e1f6..8b5daf95 100644
--- a/src/github_api_client.ts
+++ b/src/github_api_client.ts
@@ -1,5 +1,6 @@
import { soxa } from "../deps.ts";
import { UserInfo } from "./user_info.ts";
+import { CONSTANTS } from "./utils.ts";
import type {
GitHubUserActivity,
GitHubUserIssue,
@@ -8,7 +9,9 @@ import type {
} from "./user_info.ts";
export class GithubAPIClient {
- constructor() {
+ constructor(
+ private apiEndpoint: string = CONSTANTS.DEFAULT_GITHUB_API,
+ ) {
}
async requestUserInfo(
token: string | undefined,
@@ -114,7 +117,7 @@ export class GithubAPIClient {
) {
const variables = { username: username };
const response = await soxa.post(
- "https://api.github.com/graphql",
+ this.apiEndpoint,
{},
{
data: { query: query, variables },
diff --git a/src/utils.ts b/src/utils.ts
index e4067a13..9f868f76 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -62,6 +62,7 @@ export const CONSTANTS = {
DEFAULT_MARGIN_H: 0,
DEFAULT_NO_BACKGROUND: false,
DEFAULT_NO_FRAME: false,
+ DEFAULT_GITHUB_API: "https://api.github.com/graphql",
};
export enum RANK {
From fdf98a1c0e2b1af308fbd07af7658474d8716cf5 Mon Sep 17 00:00:00 2001
From: Kalle Fagerberg
Date: Fri, 12 Aug 2022 17:10:56 +0200
Subject: [PATCH 02/33] Added GITHUB_API to CONTRIBUTING.md docs
---
CONTRIBUTING.md | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f9ce3840..1e78136f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,13 +11,17 @@
Create `.env` file to project root directory, and write your GitHub token to the `.env` file.
Please select the authority of `repo` when creating token.
-```
+```properties
GITHUB_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+# if using GitHub Enterprise:
+# (this env var defaults to https://api.github.com/graphql)
+GITHUB_API=https://github.example.com/api/graphql
```
Run local server.
-```
+```sh
deno run --allow-net --allow-read --allow-env debug.ts
```
@@ -33,6 +37,6 @@ Read the [.editorconfig](./.editorconfig)
If you want to contribute to my project, you should check the lint with the following command.
-```
+```sh
deno lint --unstable
-```
\ No newline at end of file
+```
From 8a9ad5931b9f6133e4debb2fd3a61866ee3ed366 Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Thu, 8 Sep 2022 23:06:52 +0900
Subject: [PATCH 03/33] Support to multiple github token
---
index.ts | 3 +--
src/github_api_client.ts | 57 +++++++++++++++++++++-------------------
2 files changed, 31 insertions(+), 29 deletions(-)
diff --git a/index.ts b/index.ts
index 9dd46fef..2498b876 100644
--- a/index.ts
+++ b/index.ts
@@ -56,8 +56,7 @@ export default async (req: Request) => {
},
);
}
- const token = Deno.env.get("GITHUB_TOKEN");
- const userInfo = await client.requestUserInfo(token, username);
+ const userInfo = await client.requestUserInfo(username);
if (userInfo === null) {
const error = new Error404(
"Can not find a user with username: " + username,
diff --git a/src/github_api_client.ts b/src/github_api_client.ts
index 8b5daf95..af761e35 100644
--- a/src/github_api_client.ts
+++ b/src/github_api_client.ts
@@ -14,15 +14,14 @@ export class GithubAPIClient {
) {
}
async requestUserInfo(
- token: string | undefined,
username: string,
): Promise {
// Avoid timeout for the Github API
const results = await Promise.all([
- this.requestUserActivity(token, username),
- this.requestUserIssue(token, username),
- this.requestUserPullRequest(token, username),
- this.requestUserRepository(token, username),
+ this.requestUserActivity(username),
+ this.requestUserIssue(username),
+ this.requestUserPullRequest(username),
+ this.requestUserRepository(username),
]);
if (results.some((r) => r == null)) {
return null;
@@ -30,7 +29,6 @@ export class GithubAPIClient {
return new UserInfo(results[0]!, results[1]!, results[2]!, results[3]!);
}
private async requestUserActivity(
- token: string | undefined,
username: string,
): Promise {
const query = `
@@ -50,10 +48,9 @@ export class GithubAPIClient {
}
}
`;
- return await this.request(query, token, username);
+ return await this.request(query, username);
}
private async requestUserIssue(
- token: string | undefined,
username: string,
): Promise {
const query = `
@@ -68,10 +65,9 @@ export class GithubAPIClient {
}
}
`;
- return await this.request(query, token, username);
+ return await this.request(query, username);
}
private async requestUserPullRequest(
- token: string | undefined,
username: string,
): Promise {
const query = `
@@ -83,10 +79,9 @@ export class GithubAPIClient {
}
}
`;
- return await this.request(query, token, username);
+ return await this.request(query, username);
}
private async requestUserRepository(
- token: string | undefined,
username: string,
): Promise {
const query = `
@@ -108,27 +103,35 @@ export class GithubAPIClient {
}
}
`;
- return await this.request(query, token, username);
+ return await this.request(query, username);
}
private async request(
query: string,
- token: string | undefined,
username: string,
) {
+ const tokens = [
+ Deno.env.get("GITHUB_TOKEN1"),
+ Deno.env.get("GITHUB_TOKEN2"),
+ ];
const variables = { username: username };
- const response = await soxa.post(
- this.apiEndpoint,
- {},
- {
- data: { query: query, variables },
- headers: { Authorization: `bearer ${token}` },
- },
- ).catch((error) => {
- console.error(error.response.data.errors[0].message);
- });
- if (response.status != 200) {
- console.error(`Status code: ${response.status}`);
- console.error(response.data);
+ let response;
+ for (const token of tokens) {
+ response = await soxa.post(
+ this.apiEndpoint,
+ {},
+ {
+ data: { query: query, variables },
+ headers: { Authorization: `bearer ${token}` },
+ },
+ ).catch((error) => {
+ console.error(error.response.data.errors[0].message);
+ });
+ if (response.status === 200) {
+ break;
+ } else {
+ console.error(`Status code: ${response.status}`);
+ console.error(response.data);
+ }
}
return response.data.data.user;
}
From 8f09859d1beed8a6f26175e81d355ce7a41575ab Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Thu, 8 Sep 2022 23:14:20 +0900
Subject: [PATCH 04/33] Fix error response property
---
src/github_api_client.ts | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/github_api_client.ts b/src/github_api_client.ts
index af761e35..3c5d6c80 100644
--- a/src/github_api_client.ts
+++ b/src/github_api_client.ts
@@ -124,13 +124,10 @@ export class GithubAPIClient {
headers: { Authorization: `bearer ${token}` },
},
).catch((error) => {
- console.error(error.response.data.errors[0].message);
+ console.error(error.response.data);
});
- if (response.status === 200) {
+ if (response.data.data !== undefined) {
break;
- } else {
- console.error(`Status code: ${response.status}`);
- console.error(response.data);
}
}
return response.data.data.user;
From d472e5a45af5770b188ae5f87e81643d90ee54bc Mon Sep 17 00:00:00 2001
From: Aryan
Date: Wed, 14 Sep 2022 22:05:21 +0530
Subject: [PATCH 05/33] typo mistake
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 59f9d930..86379e0b 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ Ranks are `SSS` `SS` `S` `AAA` `AA` `A` `B` `C` `UNKNOWN` `SECRET`.
| ---- | ---- |
| SSS, SS, S | You are at a hard to reach rank. You can brag. |
| AAA, AA, A | You will reach this rank if you do your best. Let's aim here first. |
-| B, C | You are currently making good process. Let's aim a bit higher. |
+| B, C | You are currently making good progress. Let's aim a bit higher. |
| UNKNOWN | You have not taken action yet. Let's act first. |
| SECRET | This rank is very rare. The trophy will not be displayed until certain conditions are met. |
From 01849558711bf119a560559b5e61b64ec201736d Mon Sep 17 00:00:00 2001
From: SmashedFrenzy16 <68993968+SmashedFrenzy16@users.noreply.github.com>
Date: Thu, 19 Jan 2023 11:45:46 +0000
Subject: [PATCH 06/33] Add OG Account Trophy (#174)
* Update trophy.ts
* Update trophy_list.ts
---
src/trophy.ts | 17 +++++++++++++++++
src/trophy_list.ts | 2 ++
src/user_info.ts | 5 +++++
3 files changed, 24 insertions(+)
diff --git a/src/trophy.ts b/src/trophy.ts
index cac80cbc..ff602e98 100644
--- a/src/trophy.ts
+++ b/src/trophy.ts
@@ -199,6 +199,23 @@ export class MultipleOrganizationsTrophy extends Trophy{
}
}
+export class OGAccountTrophy extends Trophy{
+ constructor(score: number){
+ const rankConditions = [
+ new RankCondition(
+ RANK.SECRET,
+ "OG User",
+ 1,
+ ),
+ ];
+ super(score, rankConditions);
+ this.title = "OGUser";
+ this.filterTitles = ["OGUser"];
+ this.bottomMessage = "Joined 2008"
+ this.hidden = true;
+ }
+}
+
export class TotalStarTrophy extends Trophy {
constructor(score: number) {
const rankConditions = [
diff --git a/src/trophy_list.ts b/src/trophy_list.ts
index b9eaa353..6d99519e 100644
--- a/src/trophy_list.ts
+++ b/src/trophy_list.ts
@@ -9,6 +9,7 @@ import {
MultipleLangTrophy,
LongTimeAccountTrophy,
AncientAccountTrophy,
+ OGAccountTrophy,
Joined2020Trophy,
AllSuperRankTrophy,
MultipleOrganizationsTrophy,
@@ -34,6 +35,7 @@ export class TrophyList {
new MultipleLangTrophy(userInfo.languageCount),
new LongTimeAccountTrophy(userInfo.durationYear),
new AncientAccountTrophy(userInfo.ancientAccount),
+ new OGAccountTrophy(userInfo.ogAccount),
new Joined2020Trophy(userInfo.joined2020),
new MultipleOrganizationsTrophy(userInfo.totalOrganizations),
);
diff --git a/src/user_info.ts b/src/user_info.ts
index c1b25723..e65f7eb2 100644
--- a/src/user_info.ts
+++ b/src/user_info.ts
@@ -51,6 +51,7 @@ export class UserInfo {
public readonly durationYear: number;
public readonly ancientAccount: number;
public readonly joined2020: number;
+ public readonly ogAccount: number;
constructor(
userActivity: GitHubUserActivity,
userIssue: GitHubUserIssue,
@@ -85,6 +86,9 @@ export class UserInfo {
const joined2020 = new Date(userActivity.createdAt).getFullYear() == 2020
? 1
: 0;
+ const ogAccount =
+ new Date(userActivity.createdAt).getFullYear() <= 2008 ? 1 : 0;
+
this.totalCommits = totalCommits;
this.totalFollowers = userActivity.followers.totalCount;
this.totalIssues = userIssue.openIssues.totalCount + userIssue.closedIssues.totalCount;
@@ -96,5 +100,6 @@ export class UserInfo {
this.durationYear = durationYear;
this.ancientAccount = ancientAccount;
this.joined2020 = joined2020;
+ this.ogAccount = ogAccount;
}
}
From 5378858628ea199bf583f08e85987ee80897ece8 Mon Sep 17 00:00:00 2001
From: Mohan Yadav
Date: Fri, 27 Jan 2023 08:22:22 +0530
Subject: [PATCH 07/33] Update CONTRIBUTING.md (#186)
Update CONTRIBUTING.md to use the correct Github environment variables
---
CONTRIBUTING.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1e78136f..355ef0a4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,8 @@ Create `.env` file to project root directory, and write your GitHub token to the
Please select the authority of `repo` when creating token.
```properties
-GITHUB_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+GITHUB_TOKEN1=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+GITHUB_TOKEN2=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# if using GitHub Enterprise:
# (this env var defaults to https://api.github.com/graphql)
From 450f208327a0f428a5a90329b0587fdc735458c7 Mon Sep 17 00:00:00 2001
From: Bhav Beri <43399374+bhavberi@users.noreply.github.com>
Date: Thu, 18 May 2023 20:33:15 +0530
Subject: [PATCH 08/33] Added Total Reviews Trophy (#200)
---
src/github_api_client.ts | 1 +
src/trophy.ts | 50 ++++++++++++++++++++++++++++++++++++++++
src/trophy_list.ts | 2 ++
src/user_info.ts | 3 +++
4 files changed, 56 insertions(+)
diff --git a/src/github_api_client.ts b/src/github_api_client.ts
index 3c5d6c80..c23bc60c 100644
--- a/src/github_api_client.ts
+++ b/src/github_api_client.ts
@@ -38,6 +38,7 @@ export class GithubAPIClient {
contributionsCollection {
totalCommitContributions
restrictedContributionsCount
+ totalPullRequestReviewContributions
}
organizations(first: 1) {
totalCount
diff --git a/src/trophy.ts b/src/trophy.ts
index ff602e98..66d9f94f 100644
--- a/src/trophy.ts
+++ b/src/trophy.ts
@@ -216,6 +216,56 @@ export class OGAccountTrophy extends Trophy{
}
}
+export class TotalReviewsTrophy extends Trophy {
+ constructor(score: number) {
+ const rankConditions = [
+ new RankCondition(
+ RANK.SSS,
+ "God Reviewer",
+ 70,
+ ),
+ new RankCondition(
+ RANK.SS,
+ "Deep Reviewer",
+ 57,
+ ),
+ new RankCondition(
+ RANK.S,
+ "Super Reviewer",
+ 45,
+ ),
+ new RankCondition(
+ RANK.AAA,
+ "Ultra Reviewer",
+ 30,
+ ),
+ new RankCondition(
+ RANK.AA,
+ "Hyper Reviewer",
+ 20,
+ ),
+ new RankCondition(
+ RANK.A,
+ "Active Reviewer",
+ 8,
+ ),
+ new RankCondition(
+ RANK.B,
+ "Intermediate Reviewer",
+ 3,
+ ),
+ new RankCondition(
+ RANK.C,
+ "New Reviewer",
+ 1,
+ ),
+ ];
+ super(score, rankConditions);
+ this.title = "Reviews";
+ this.filterTitles = ["Review", "Reviews"];
+ }
+}
+
export class TotalStarTrophy extends Trophy {
constructor(score: number) {
const rankConditions = [
diff --git a/src/trophy_list.ts b/src/trophy_list.ts
index 6d99519e..7b5b9c51 100644
--- a/src/trophy_list.ts
+++ b/src/trophy_list.ts
@@ -6,6 +6,7 @@ import {
TotalIssueTrophy,
TotalPullRequestTrophy,
TotalRepositoryTrophy,
+ TotalReviewsTrophy,
MultipleLangTrophy,
LongTimeAccountTrophy,
AncientAccountTrophy,
@@ -28,6 +29,7 @@ export class TrophyList {
new TotalIssueTrophy(userInfo.totalIssues),
new TotalPullRequestTrophy(userInfo.totalPullRequests),
new TotalRepositoryTrophy(userInfo.totalRepositories),
+ new TotalReviewsTrophy(userInfo.totalReviews),
);
// Secret trophies
this.trophies.push(
diff --git a/src/user_info.ts b/src/user_info.ts
index e65f7eb2..c2e47c3b 100644
--- a/src/user_info.ts
+++ b/src/user_info.ts
@@ -31,6 +31,7 @@ export type GitHubUserActivity = {
contributionsCollection: {
totalCommitContributions: number;
restrictedContributionsCount: number;
+ totalPullRequestReviewContributions: number;
};
organizations: {
totalCount: number;
@@ -45,6 +46,7 @@ export class UserInfo {
public readonly totalIssues: number;
public readonly totalOrganizations: number;
public readonly totalPullRequests: number;
+ public readonly totalReviews: number;
public readonly totalStargazers: number;
public readonly totalRepositories: number;
public readonly languageCount: number;
@@ -94,6 +96,7 @@ export class UserInfo {
this.totalIssues = userIssue.openIssues.totalCount + userIssue.closedIssues.totalCount;
this.totalOrganizations = userActivity.organizations.totalCount;
this.totalPullRequests = userPullRequest.pullRequests.totalCount;
+ this.totalReviews = userActivity.contributionsCollection.totalPullRequestReviewContributions;
this.totalStargazers = totalStargazers;
this.totalRepositories = userRepository.repositories.totalCount;
this.languageCount = languages.size;
From 33b4105d8af3abeb5559286aec9093945a52efa4 Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Fri, 19 May 2023 08:57:42 +0900
Subject: [PATCH 09/33] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 86379e0b..4056815f 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ Add the following code to your readme. When pasting the code into your profile's
```
-
+
## Use theme
From 22e46c69e7f9981ac152b10af329f2839378095c Mon Sep 17 00:00:00 2001
From: Alexandro Castro <10711649+AlexcastroDev@users.noreply.github.com>
Date: Tue, 26 Sep 2023 14:59:20 +0100
Subject: [PATCH 10/33] feat: implement staticRenderRegeneration (#221)
* feat: implement staticRenderRegeneration
* chore: remove the check in create
* chore: ignore read permission throw
* chore: safe read file
* fix: stream save
* chore: change file folder
---
CONTRIBUTING.md | 2 +-
index.ts | 26 +++++++---
src/StaticRenderRegeneration/cache_manager.ts | 49 +++++++++++++++++++
src/StaticRenderRegeneration/index.ts | 29 +++++++++++
src/StaticRenderRegeneration/types.ts | 6 +++
src/StaticRenderRegeneration/utils.ts | 42 ++++++++++++++++
src/utils.ts | 3 ++
7 files changed, 148 insertions(+), 9 deletions(-)
create mode 100644 src/StaticRenderRegeneration/cache_manager.ts
create mode 100644 src/StaticRenderRegeneration/index.ts
create mode 100644 src/StaticRenderRegeneration/types.ts
create mode 100644 src/StaticRenderRegeneration/utils.ts
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 355ef0a4..95eaad16 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,7 +23,7 @@ GITHUB_API=https://github.example.com/api/graphql
Run local server.
```sh
-deno run --allow-net --allow-read --allow-env debug.ts
+deno run --allow-net --allow-read --allow-env --allow-write debug.ts
```
Open localhost from your browser.
diff --git a/index.ts b/index.ts
index 2498b876..5f7f6cda 100644
--- a/index.ts
+++ b/index.ts
@@ -4,11 +4,25 @@ import { CONSTANTS, parseParams } from "./src/utils.ts";
import { COLORS, Theme } from "./src/theme.ts";
import { Error400, Error404 } from "./src/error_page.ts";
import "https://deno.land/x/dotenv@v0.5.0/load.ts";
-
+import {staticRenderRegeneration} from "./src/StaticRenderRegeneration/index.ts";
const apiEndpoint = Deno.env.get("GITHUB_API") || CONSTANTS.DEFAULT_GITHUB_API;
const client = new GithubAPIClient(apiEndpoint);
-export default async (req: Request) => {
+const defaultHeaders = new Headers(
+ {
+ "Content-Type": "image/svg+xml",
+ "Cache-Control": `public, max-age=${CONSTANTS.CACHE_MAX_AGE}`,
+ },
+)
+
+export default (request: Request) => staticRenderRegeneration(request, {
+ revalidate: CONSTANTS.REVALIDATE_TIME,
+ headers: defaultHeaders
+}, function (req: Request) {
+ return app(req);
+});
+
+async function app (req: Request): Promise{
const params = parseParams(req);
const username = params.get("username");
const row = params.getNumberValue("row", CONSTANTS.DEFAULT_MAX_ROW);
@@ -69,6 +83,7 @@ export default async (req: Request) => {
},
);
}
+
// Success Response
return new Response(
new Card(
@@ -83,12 +98,7 @@ export default async (req: Request) => {
noFrame,
).render(userInfo, theme),
{
- headers: new Headers(
- {
- "Content-Type": "image/svg+xml",
- "Cache-Control": `public, max-age=${CONSTANTS.CACHE_MAX_AGE}`,
- },
- ),
+ headers: defaultHeaders,
},
);
};
diff --git a/src/StaticRenderRegeneration/cache_manager.ts b/src/StaticRenderRegeneration/cache_manager.ts
new file mode 100644
index 00000000..3a93e4b4
--- /dev/null
+++ b/src/StaticRenderRegeneration/cache_manager.ts
@@ -0,0 +1,49 @@
+import { existsSync } from './utils.ts'
+
+export class CacheManager {
+ constructor(private revalidateTime: number, private cacheFile: string) {}
+
+ // Reason to use /tmp/:
+ // https://github.com/orgs/vercel/discussions/314
+ get cacheFilePath(): string {
+ return `/tmp/${this.cacheFile}`;
+ }
+ get cacheFileExists(): boolean {
+ return existsSync(this.cacheFilePath);
+ }
+
+ get cacheFileLastModified(): Date | null {
+ if (!this.cacheFileExists) {
+ return null;
+ }
+ const fileInfo = Deno.statSync(this.cacheFilePath);
+ return fileInfo.mtime ?? null;
+ }
+
+ get cacheFileLastModifiedGetTime(): number | null {
+ const lastModified = this.cacheFileLastModified;
+ if (lastModified === null) {
+ return null;
+ }
+ return lastModified.getTime();
+ }
+
+ get isCacheValid(): boolean {
+ if (this.cacheFileLastModifiedGetTime === null) {
+ return false;
+ }
+ const currentTime = new Date().getTime();
+ return currentTime - this.cacheFileLastModifiedGetTime < this.revalidateTime;
+ }
+
+ async save (response: Response): Promise {
+ if(response === null) return
+ // Prevent TypeError: ReadableStream is locked
+ const text = await response.clone().text()
+ const data = new TextEncoder().encode(text)
+
+ Deno.writeFile(this.cacheFilePath, data, { create: true }).catch(() => {
+ console.warn("Failed to save cache file")
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/StaticRenderRegeneration/index.ts b/src/StaticRenderRegeneration/index.ts
new file mode 100644
index 00000000..d93fc61f
--- /dev/null
+++ b/src/StaticRenderRegeneration/index.ts
@@ -0,0 +1,29 @@
+import { CacheManager } from "./cache_manager.ts";
+import { StaticRegenerationOptions } from "./types.ts";
+import { getUrl, readCache, generateUUID } from "./utils.ts";
+
+export async function staticRenderRegeneration(request: Request, options: StaticRegenerationOptions, render: (request: Request) => Promise) {
+ // avoid TypeError: Invalid URL at deno:core
+ const url = getUrl(request)
+
+ // if more conditions are added, make sure to create a variable to skipCache
+ if (url.pathname === "/favicon.ico") {
+ return await render(request);
+ }
+
+ const cacheFile = await generateUUID(url.pathname + (url.search ?? ""));
+ const cacheManager = new CacheManager(options.revalidate ?? 0, cacheFile);
+ if (cacheManager.isCacheValid) {
+ const cache = readCache(cacheManager.cacheFilePath)
+ if(cache !== null) {
+ return new Response(cache, {
+ headers: options.headers ?? new Headers({}),
+ });
+ }
+ }
+
+ const response = await render(request)
+ cacheManager.save(response)
+
+ return response
+}
diff --git a/src/StaticRenderRegeneration/types.ts b/src/StaticRenderRegeneration/types.ts
new file mode 100644
index 00000000..e9b79076
--- /dev/null
+++ b/src/StaticRenderRegeneration/types.ts
@@ -0,0 +1,6 @@
+export interface StaticRegenerationOptions {
+ // The number of milliseconds before the page should be revalidated
+ revalidate?: number
+ // The headers to be sent with the response
+ headers?: Headers
+}
\ No newline at end of file
diff --git a/src/StaticRenderRegeneration/utils.ts b/src/StaticRenderRegeneration/utils.ts
new file mode 100644
index 00000000..ebc19c4b
--- /dev/null
+++ b/src/StaticRenderRegeneration/utils.ts
@@ -0,0 +1,42 @@
+export function getUrl(request: Request) {
+ try {
+ return new URL(request.url)
+ } catch {
+ return {
+ pathname: request.url,
+ search: request.url
+ }
+ }
+}
+
+export function readCache(cacheFilePath: string): Uint8Array | null {
+ try {
+ return Deno.readFileSync(cacheFilePath)
+ } catch {
+ return null
+ }
+}
+
+// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
+export async function generateUUID(message: string): Promise {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(message);
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
+
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+
+ return hashHex;
+}
+
+
+export const existsSync = (filename: string): boolean => {
+ try {
+ Deno.statSync(filename);
+ // successful, file or directory must exist
+ return true;
+ } catch {
+ return false;
+ }
+};
+
\ No newline at end of file
diff --git a/src/utils.ts b/src/utils.ts
index 9f868f76..3b650435 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -53,6 +53,8 @@ export function abridgeScore(score: number): string {
return (Math.sign(score) * Math.abs(score)).toString() + "pt";
}
+const HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
+
export const CONSTANTS = {
CACHE_MAX_AGE: 7200,
DEFAULT_PANEL_SIZE: 110,
@@ -63,6 +65,7 @@ export const CONSTANTS = {
DEFAULT_NO_BACKGROUND: false,
DEFAULT_NO_FRAME: false,
DEFAULT_GITHUB_API: "https://api.github.com/graphql",
+ REVALIDATE_TIME: HOUR_IN_MILLISECONDS,
};
export enum RANK {
From 85f806480710de6f70b4f6c15f9991a068159aee Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Tue, 26 Sep 2023 23:29:10 +0900
Subject: [PATCH 11/33] Update README.md
From a4d50a11dbd6560527243202cd5ca5c9782daf9a Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Thu, 28 Sep 2023 00:56:48 +0900
Subject: [PATCH 12/33] Fixed the number of Github API requests that were being
executed twice.
---
index.ts | 2 +-
src/github_api_client.ts | 51 +++++++++++++++++++++++++++++-----------
src/utils.ts | 1 +
3 files changed, 39 insertions(+), 15 deletions(-)
diff --git a/index.ts b/index.ts
index 5f7f6cda..0bbbcce8 100644
--- a/index.ts
+++ b/index.ts
@@ -101,4 +101,4 @@ async function app (req: Request): Promise{
headers: defaultHeaders,
},
);
-};
+}
diff --git a/src/github_api_client.ts b/src/github_api_client.ts
index c23bc60c..c4b8361e 100644
--- a/src/github_api_client.ts
+++ b/src/github_api_client.ts
@@ -24,6 +24,7 @@ export class GithubAPIClient {
this.requestUserRepository(username),
]);
if (results.some((r) => r == null)) {
+ console.error(`Can not find a user with username:'${username}'`);
return null;
}
return new UserInfo(results[0]!, results[1]!, results[2]!, results[3]!);
@@ -109,28 +110,50 @@ export class GithubAPIClient {
private async request(
query: string,
username: string,
+ retryDelay = CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY,
) {
const tokens = [
Deno.env.get("GITHUB_TOKEN1"),
Deno.env.get("GITHUB_TOKEN2"),
];
+ const maxRetries = tokens.length;
+
const variables = { username: username };
let response;
- for (const token of tokens) {
- response = await soxa.post(
- this.apiEndpoint,
- {},
- {
- data: { query: query, variables },
- headers: { Authorization: `bearer ${token}` },
- },
- ).catch((error) => {
- console.error(error.response.data);
- });
- if (response.data.data !== undefined) {
- break;
+
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
+ try {
+ response = await soxa.post(
+ this.apiEndpoint,
+ {},
+ {
+ data: { query: query, variables },
+ headers: { Authorization: `bearer ${tokens[attempt]}` },
+ },
+ );
+ if (response.data.errors !== undefined) {
+ throw new Error(
+ response.data.errors.map((e: { message: string; type: string }) =>
+ e.message
+ ).join("\n"),
+ );
+ }
+ if (response.data.data !== undefined) {
+ return response.data.data.user;
+ } else {
+ return null;
+ }
+ } catch (error) {
+ console.error(
+ `Attempt ${attempt} failed with GITHUB_TOKEN${attempt + 1}:`,
+ error,
+ );
}
+
+ console.log(`Retrying in ${retryDelay / 1000} seconds...`);
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
- return response.data.data.user;
+
+ throw new Error(`Max retries (${maxRetries}) exceeded.`);
}
}
diff --git a/src/utils.ts b/src/utils.ts
index 3b650435..0ed5cd30 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -65,6 +65,7 @@ export const CONSTANTS = {
DEFAULT_NO_BACKGROUND: false,
DEFAULT_NO_FRAME: false,
DEFAULT_GITHUB_API: "https://api.github.com/graphql",
+ DEFAULT_GITHUB_RETRY_DELAY: 1000,
REVALIDATE_TIME: HOUR_IN_MILLISECONDS,
};
From 5dc3379214adb0b9e53b9d7a87238e6d880cea87 Mon Sep 17 00:00:00 2001
From: Alexandro Castro <10711649+AlexcastroDev@users.noreply.github.com>
Date: Fri, 29 Sep 2023 17:54:23 +0100
Subject: [PATCH 13/33] Improvements on workflow and files (#222)
* chore: improvements
* chore: add testing workflow
* fix: types and lint
* chore: change dynamic
* chore: improvements
* chore: no need repository to check status
---
.github/workflows/testing.yml | 22 +++
README.md | 6 +
index.ts => api/index.ts | 37 ++--
debug.ts | 2 +-
deps.ts | 22 ++-
src/Helpers/Retry.ts | 44 +++++
src/Helpers/__tests__/Retry.test.ts | 66 ++++++++
src/Repository/GithubRepository.ts | 25 +++
src/Schemas/index.ts | 61 +++++++
src/Services/GithubApiService.ts | 120 +++++++++++++
src/StaticRenderRegeneration/cache_manager.ts | 81 ++++-----
src/Types/Request.ts | 5 +
src/Types/index.ts | 1 +
src/github_api_client.ts | 159 ------------------
vercel.json | 19 ++-
15 files changed, 446 insertions(+), 224 deletions(-)
create mode 100644 .github/workflows/testing.yml
rename index.ts => api/index.ts (73%)
create mode 100644 src/Helpers/Retry.ts
create mode 100644 src/Helpers/__tests__/Retry.test.ts
create mode 100644 src/Repository/GithubRepository.ts
create mode 100644 src/Schemas/index.ts
create mode 100644 src/Services/GithubApiService.ts
create mode 100644 src/Types/Request.ts
create mode 100644 src/Types/index.ts
delete mode 100644 src/github_api_client.ts
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
new file mode 100644
index 00000000..b8215530
--- /dev/null
+++ b/.github/workflows/testing.yml
@@ -0,0 +1,22 @@
+name: Check PR Test
+
+on:
+ pull_request:
+ branches:
+ - master
+jobs:
+ install-dependencies:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check if PR is in draft mode
+ run: |
+ if [ "${{ github.event.pull_request.draft }}" == "true" ]; then
+ echo "PR is in draft mode, skipping workflow"
+ exit 78
+ fi
+ - name: Set up Deno
+ uses: denolib/setup-deno@v2
+ with:
+ deno-version: "1.37.0"
+ - name: Run tests
+ run: deno test --allow-env
diff --git a/README.md b/README.md
index 4056815f..b4727c8b 100644
--- a/README.md
+++ b/README.md
@@ -498,3 +498,9 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&no-frame=true
# Contribution Guide
Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
+
+# Testing
+
+```bash
+deno test --allow-env
+```
diff --git a/index.ts b/api/index.ts
similarity index 73%
rename from index.ts
rename to api/index.ts
index 0bbbcce8..4a095c78 100644
--- a/index.ts
+++ b/api/index.ts
@@ -1,28 +1,31 @@
-import { GithubAPIClient } from "./src/github_api_client.ts";
-import { Card } from "./src/card.ts";
-import { CONSTANTS, parseParams } from "./src/utils.ts";
-import { COLORS, Theme } from "./src/theme.ts";
-import { Error400, Error404 } from "./src/error_page.ts";
+import { Card } from "../src/card.ts";
+import { CONSTANTS, parseParams } from "../src/utils.ts";
+import { COLORS, Theme } from "../src/theme.ts";
+import { Error400, Error404 } from "../src/error_page.ts";
import "https://deno.land/x/dotenv@v0.5.0/load.ts";
-import {staticRenderRegeneration} from "./src/StaticRenderRegeneration/index.ts";
-const apiEndpoint = Deno.env.get("GITHUB_API") || CONSTANTS.DEFAULT_GITHUB_API;
-const client = new GithubAPIClient(apiEndpoint);
+import { staticRenderRegeneration } from "../src/StaticRenderRegeneration/index.ts";
+import { GithubRepositoryService } from "../src/Repository/GithubRepository.ts";
+import { GithubApiService } from "../src/Services/GithubApiService.ts";
+
+const serviceProvider = new GithubApiService();
+const client = new GithubRepositoryService(serviceProvider).repository;
const defaultHeaders = new Headers(
{
"Content-Type": "image/svg+xml",
"Cache-Control": `public, max-age=${CONSTANTS.CACHE_MAX_AGE}`,
},
-)
+);
-export default (request: Request) => staticRenderRegeneration(request, {
- revalidate: CONSTANTS.REVALIDATE_TIME,
- headers: defaultHeaders
-}, function (req: Request) {
- return app(req);
-});
+export default (request: Request) =>
+ staticRenderRegeneration(request, {
+ revalidate: CONSTANTS.REVALIDATE_TIME,
+ headers: defaultHeaders,
+ }, function (req: Request) {
+ return app(req);
+ });
-async function app (req: Request): Promise{
+async function app(req: Request): Promise {
const params = parseParams(req);
const username = params.get("username");
const row = params.getNumberValue("row", CONSTANTS.DEFAULT_MAX_ROW);
@@ -83,7 +86,7 @@ async function app (req: Request): Promise{
},
);
}
-
+
// Success Response
return new Response(
new Card(
diff --git a/debug.ts b/debug.ts
index ab64cc8a..4d1ac0b0 100644
--- a/debug.ts
+++ b/debug.ts
@@ -1,4 +1,4 @@
import { serve } from "https://deno.land/std@0.125.0/http/server.ts";
-import requestHandler from "./index.ts";
+import requestHandler from "./api/index.ts";
serve(requestHandler, { port: 8080 });
diff --git a/deps.ts b/deps.ts
index 5d6175aa..4b51bfce 100644
--- a/deps.ts
+++ b/deps.ts
@@ -1 +1,21 @@
-export { soxa } from "https://deno.land/x/soxa@1.4/mod.ts";
+import { Soxa as ServiceProvider } from "https://deno.land/x/soxa@1.4/src/core/Soxa.ts";
+import { defaults } from "https://deno.land/x/soxa@1.4/src/defaults.ts";
+import {
+ assertEquals,
+ assertRejects,
+} from "https://deno.land/std@0.203.0/assert/mod.ts";
+import {
+ assertSpyCalls,
+ spy,
+} from "https://deno.land/std@0.203.0/testing/mock.ts";
+
+import { CONSTANTS } from "./src/utils.ts";
+
+const baseURL = Deno.env.get("GITHUB_API") || CONSTANTS.DEFAULT_GITHUB_API;
+
+const soxa = new ServiceProvider({
+ ...defaults,
+ baseURL,
+});
+
+export { assertEquals, assertRejects, assertSpyCalls, soxa, spy };
diff --git a/src/Helpers/Retry.ts b/src/Helpers/Retry.ts
new file mode 100644
index 00000000..be1eb9fc
--- /dev/null
+++ b/src/Helpers/Retry.ts
@@ -0,0 +1,44 @@
+export type RetryCallbackProps = {
+ attempt: number;
+};
+
+type callbackType = (data: RetryCallbackProps) => Promise | T;
+
+async function* createAsyncIterable(
+ callback: callbackType,
+ retries: number,
+ delay: number,
+) {
+ for (let i = 0; i < retries; i++) {
+ try {
+ const data = await callback({ attempt: i });
+ yield data;
+ return;
+ } catch (e) {
+ yield null;
+ console.error(e);
+ await new Promise((resolve) => setTimeout(resolve, delay));
+ }
+ }
+}
+
+export class Retry {
+ constructor(private maxRetries = 2, private retryDelay = 1000) {}
+ async fetch(
+ callback: callbackType,
+ ) {
+ for await (
+ const callbackResult of createAsyncIterable(
+ callback,
+ this.maxRetries,
+ this.retryDelay,
+ )
+ ) {
+ if (callbackResult) {
+ return callbackResult as T;
+ }
+ }
+
+ throw new Error(`Max retries (${this.maxRetries}) exceeded.`);
+ }
+}
diff --git a/src/Helpers/__tests__/Retry.test.ts b/src/Helpers/__tests__/Retry.test.ts
new file mode 100644
index 00000000..7fcad13e
--- /dev/null
+++ b/src/Helpers/__tests__/Retry.test.ts
@@ -0,0 +1,66 @@
+import { Retry } from "../Retry.ts";
+import {
+ assertEquals,
+ assertRejects,
+ assertSpyCalls,
+ spy,
+} from "../../../deps.ts";
+
+type MockResponse = {
+ value: number;
+};
+
+Deno.test("Retry.fetch", () => {
+ const retryInstance = new Retry();
+ const callback = spy(retryInstance, "fetch");
+
+ retryInstance.fetch(() => {
+ return { value: 1 };
+ });
+
+ assertSpyCalls(callback, 1);
+});
+
+Deno.test("Should retry", async () => {
+ let countErrors = 0;
+
+ const callbackError = () => {
+ countErrors++;
+ throw new Error("Panic! Threw Error");
+ };
+ const retries = 3;
+ const retryInstance = new Retry(retries);
+
+ await assertRejects(
+ () => {
+ return retryInstance.fetch(callbackError);
+ },
+ Error,
+ `Max retries (${retries}) exceeded.`,
+ );
+
+ assertEquals(countErrors, 3);
+});
+
+Deno.test("Should retry the asyncronous callback", async () => {
+ let countErrors = 0;
+
+ const callbackError = async () => {
+ countErrors++;
+ // Mock request in callback
+ await new Promise((_, reject) => setTimeout(reject, 100));
+ };
+
+ const retries = 3;
+ const retryInstance = new Retry(retries);
+
+ await assertRejects(
+ () => {
+ return retryInstance.fetch(callbackError);
+ },
+ Error,
+ `Max retries (${retries}) exceeded.`,
+ );
+
+ assertEquals(countErrors, 3);
+});
diff --git a/src/Repository/GithubRepository.ts b/src/Repository/GithubRepository.ts
new file mode 100644
index 00000000..b345fd35
--- /dev/null
+++ b/src/Repository/GithubRepository.ts
@@ -0,0 +1,25 @@
+import {
+ GitHubUserActivity,
+ GitHubUserIssue,
+ GitHubUserPullRequest,
+ GitHubUserRepository,
+ UserInfo,
+} from "../user_info.ts";
+
+export abstract class GithubRepository {
+ abstract requestUserInfo(username: string): Promise;
+ abstract requestUserActivity(
+ username: string,
+ ): Promise;
+ abstract requestUserIssue(username: string): Promise;
+ abstract requestUserPullRequest(
+ username: string,
+ ): Promise;
+ abstract requestUserRepository(
+ username: string,
+ ): Promise;
+}
+
+export class GithubRepositoryService {
+ constructor(public repository: GithubRepository) {}
+}
diff --git a/src/Schemas/index.ts b/src/Schemas/index.ts
new file mode 100644
index 00000000..2c7e3f34
--- /dev/null
+++ b/src/Schemas/index.ts
@@ -0,0 +1,61 @@
+export const queryUserActivity = `
+ query userInfo($username: String!) {
+ user(login: $username) {
+ createdAt
+ contributionsCollection {
+ totalCommitContributions
+ restrictedContributionsCount
+ totalPullRequestReviewContributions
+ }
+ organizations(first: 1) {
+ totalCount
+ }
+ followers(first: 1) {
+ totalCount
+ }
+ }
+ }
+`;
+
+export const queryUserIssue = `
+ query userInfo($username: String!) {
+ user(login: $username) {
+ openIssues: issues(states: OPEN) {
+ totalCount
+ }
+ closedIssues: issues(states: CLOSED) {
+ totalCount
+ }
+ }
+ }
+`;
+
+export const queryUserPullRequest = `
+ query userInfo($username: String!) {
+ user(login: $username) {
+ pullRequests(first: 1) {
+ totalCount
+ }
+ }
+ }
+`;
+
+export const queryUserRepository = `
+ query userInfo($username: String!) {
+ user(login: $username) {
+ repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) {
+ totalCount
+ nodes {
+ languages(first: 3, orderBy: {direction:DESC, field: SIZE}) {
+ nodes {
+ name
+ }
+ }
+ stargazers {
+ totalCount
+ }
+ }
+ }
+ }
+ }
+`;
diff --git a/src/Services/GithubApiService.ts b/src/Services/GithubApiService.ts
new file mode 100644
index 00000000..4d7a9ccd
--- /dev/null
+++ b/src/Services/GithubApiService.ts
@@ -0,0 +1,120 @@
+import { GithubRepository } from "../Repository/GithubRepository.ts";
+import {
+ GitHubUserActivity,
+ GitHubUserIssue,
+ GitHubUserPullRequest,
+ GitHubUserRepository,
+ UserInfo,
+} from "../user_info.ts";
+import {
+ queryUserActivity,
+ queryUserIssue,
+ queryUserPullRequest,
+ queryUserRepository,
+} from "../Schemas/index.ts";
+import { soxa } from "../../deps.ts";
+import { Retry } from "../Helpers/Retry.ts";
+import { QueryDefaultResponse } from "../Types/index.ts";
+import { CONSTANTS } from "../utils.ts";
+
+// Need to be here - Exporting from another file makes array of null
+export const TOKENS = [
+ Deno.env.get("GITHUB_TOKEN1"),
+ Deno.env.get("GITHUB_TOKEN2"),
+];
+
+export class GithubApiService extends GithubRepository {
+ async requestUserRepository(
+ username: string,
+ ): Promise {
+ return await this.executeQuery(queryUserRepository, {
+ username,
+ });
+ }
+ async requestUserActivity(
+ username: string,
+ ): Promise {
+ return await this.executeQuery(queryUserActivity, {
+ username,
+ });
+ }
+ async requestUserIssue(username: string): Promise {
+ return await this.executeQuery(queryUserIssue, {
+ username,
+ });
+ }
+ async requestUserPullRequest(
+ username: string,
+ ): Promise {
+ return await this.executeQuery(
+ queryUserPullRequest,
+ { username },
+ );
+ }
+ async requestUserInfo(username: string): Promise {
+ // Avoid to call others if one of them is null
+ const repository = await this.requestUserRepository(username);
+ if (repository === null) return null;
+
+ const promises = Promise.allSettled([
+ this.requestUserActivity(username),
+ this.requestUserIssue(username),
+ this.requestUserPullRequest(username),
+ ]);
+ const [activity, issue, pullRequest] = await promises;
+ const status = [
+ activity.status,
+ issue.status,
+ pullRequest.status,
+ ];
+
+ if (status.includes("rejected")) {
+ console.error(`Can not find a user with username:' ${username}'`);
+ return null;
+ }
+
+ return new UserInfo(
+ (activity as PromiseFulfilledResult).value,
+ (issue as PromiseFulfilledResult).value,
+ (pullRequest as PromiseFulfilledResult).value,
+ repository,
+ );
+ }
+
+ async executeQuery(
+ query: string,
+ variables: { [key: string]: string },
+ ) {
+ try {
+ const retry = new Retry(
+ TOKENS.length,
+ CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY,
+ );
+ const response = await retry.fetch>(async ({ attempt }) => {
+ return await soxa.post("", {}, {
+ data: { query: query, variables },
+ headers: {
+ Authorization: `bearer ${TOKENS[attempt]}`,
+ },
+ });
+ }) as QueryDefaultResponse<{ user: T; errors?: unknown[] }>;
+
+ if (response.data.data.errors) {
+ throw new Error("Error from Github API", {
+ cause: response.data.data.errors,
+ });
+ }
+
+ return response?.data?.data?.user ?? null;
+ } catch (error) {
+ // TODO: Move this to a logger instance later
+ if (error instanceof Error && error.cause) {
+ console.error(JSON.stringify(error.cause, null, 2));
+ } else {
+ console.error(error);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/StaticRenderRegeneration/cache_manager.ts b/src/StaticRenderRegeneration/cache_manager.ts
index 3a93e4b4..2f11479c 100644
--- a/src/StaticRenderRegeneration/cache_manager.ts
+++ b/src/StaticRenderRegeneration/cache_manager.ts
@@ -1,49 +1,50 @@
-import { existsSync } from './utils.ts'
+import { existsSync } from "./utils.ts";
export class CacheManager {
- constructor(private revalidateTime: number, private cacheFile: string) {}
-
- // Reason to use /tmp/:
- // https://github.com/orgs/vercel/discussions/314
- get cacheFilePath(): string {
- return `/tmp/${this.cacheFile}`;
- }
- get cacheFileExists(): boolean {
- return existsSync(this.cacheFilePath);
- }
+ constructor(private revalidateTime: number, private cacheFile: string) {}
- get cacheFileLastModified(): Date | null {
- if (!this.cacheFileExists) {
- return null;
- }
- const fileInfo = Deno.statSync(this.cacheFilePath);
- return fileInfo.mtime ?? null;
- }
+ // Reason to use /tmp/:
+ // https://github.com/orgs/vercel/discussions/314
+ get cacheFilePath(): string {
+ return `/tmp/${this.cacheFile}`;
+ }
+ get cacheFileExists(): boolean {
+ return existsSync(this.cacheFilePath);
+ }
- get cacheFileLastModifiedGetTime(): number | null {
- const lastModified = this.cacheFileLastModified;
- if (lastModified === null) {
- return null;
- }
- return lastModified.getTime();
+ get cacheFileLastModified(): Date | null {
+ if (!this.cacheFileExists) {
+ return null;
}
+ const fileInfo = Deno.statSync(this.cacheFilePath);
+ return fileInfo.mtime ?? null;
+ }
- get isCacheValid(): boolean {
- if (this.cacheFileLastModifiedGetTime === null) {
- return false;
- }
- const currentTime = new Date().getTime();
- return currentTime - this.cacheFileLastModifiedGetTime < this.revalidateTime;
+ get cacheFileLastModifiedGetTime(): number | null {
+ const lastModified = this.cacheFileLastModified;
+ if (lastModified === null) {
+ return null;
}
+ return lastModified.getTime();
+ }
- async save (response: Response): Promise {
- if(response === null) return
- // Prevent TypeError: ReadableStream is locked
- const text = await response.clone().text()
- const data = new TextEncoder().encode(text)
-
- Deno.writeFile(this.cacheFilePath, data, { create: true }).catch(() => {
- console.warn("Failed to save cache file")
- });
+ get isCacheValid(): boolean {
+ if (this.cacheFileLastModifiedGetTime === null) {
+ return false;
}
-}
\ No newline at end of file
+ const currentTime = new Date().getTime();
+ return currentTime - this.cacheFileLastModifiedGetTime <
+ this.revalidateTime;
+ }
+
+ async save(response: Response): Promise {
+ if (response === null) return;
+ // Prevent TypeError: ReadableStream is locked
+ const text = await response.clone().text();
+ const data = new TextEncoder().encode(text);
+
+ Deno.writeFile(this.cacheFilePath, data, { create: true }).catch(() => {
+ console.warn("Failed to save cache file");
+ });
+ }
+}
diff --git a/src/Types/Request.ts b/src/Types/Request.ts
new file mode 100644
index 00000000..5ef517e7
--- /dev/null
+++ b/src/Types/Request.ts
@@ -0,0 +1,5 @@
+export type QueryDefaultResponse = {
+ data: {
+ data: T;
+ };
+};
diff --git a/src/Types/index.ts b/src/Types/index.ts
new file mode 100644
index 00000000..07f4ba9c
--- /dev/null
+++ b/src/Types/index.ts
@@ -0,0 +1 @@
+export * from './Request.ts'
\ No newline at end of file
diff --git a/src/github_api_client.ts b/src/github_api_client.ts
deleted file mode 100644
index c4b8361e..00000000
--- a/src/github_api_client.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { soxa } from "../deps.ts";
-import { UserInfo } from "./user_info.ts";
-import { CONSTANTS } from "./utils.ts";
-import type {
- GitHubUserActivity,
- GitHubUserIssue,
- GitHubUserPullRequest,
- GitHubUserRepository,
-} from "./user_info.ts";
-
-export class GithubAPIClient {
- constructor(
- private apiEndpoint: string = CONSTANTS.DEFAULT_GITHUB_API,
- ) {
- }
- async requestUserInfo(
- username: string,
- ): Promise {
- // Avoid timeout for the Github API
- const results = await Promise.all([
- this.requestUserActivity(username),
- this.requestUserIssue(username),
- this.requestUserPullRequest(username),
- this.requestUserRepository(username),
- ]);
- if (results.some((r) => r == null)) {
- console.error(`Can not find a user with username:'${username}'`);
- return null;
- }
- return new UserInfo(results[0]!, results[1]!, results[2]!, results[3]!);
- }
- private async requestUserActivity(
- username: string,
- ): Promise {
- const query = `
- query userInfo($username: String!) {
- user(login: $username) {
- createdAt
- contributionsCollection {
- totalCommitContributions
- restrictedContributionsCount
- totalPullRequestReviewContributions
- }
- organizations(first: 1) {
- totalCount
- }
- followers(first: 1) {
- totalCount
- }
- }
- }
- `;
- return await this.request(query, username);
- }
- private async requestUserIssue(
- username: string,
- ): Promise {
- const query = `
- query userInfo($username: String!) {
- user(login: $username) {
- openIssues: issues(states: OPEN) {
- totalCount
- }
- closedIssues: issues(states: CLOSED) {
- totalCount
- }
- }
- }
- `;
- return await this.request(query, username);
- }
- private async requestUserPullRequest(
- username: string,
- ): Promise {
- const query = `
- query userInfo($username: String!) {
- user(login: $username) {
- pullRequests(first: 1) {
- totalCount
- }
- }
- }
- `;
- return await this.request(query, username);
- }
- private async requestUserRepository(
- username: string,
- ): Promise {
- const query = `
- query userInfo($username: String!) {
- user(login: $username) {
- repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) {
- totalCount
- nodes {
- languages(first: 3, orderBy: {direction:DESC, field: SIZE}) {
- nodes {
- name
- }
- }
- stargazers {
- totalCount
- }
- }
- }
- }
- }
- `;
- return await this.request(query, username);
- }
- private async request(
- query: string,
- username: string,
- retryDelay = CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY,
- ) {
- const tokens = [
- Deno.env.get("GITHUB_TOKEN1"),
- Deno.env.get("GITHUB_TOKEN2"),
- ];
- const maxRetries = tokens.length;
-
- const variables = { username: username };
- let response;
-
- for (let attempt = 0; attempt < maxRetries; attempt++) {
- try {
- response = await soxa.post(
- this.apiEndpoint,
- {},
- {
- data: { query: query, variables },
- headers: { Authorization: `bearer ${tokens[attempt]}` },
- },
- );
- if (response.data.errors !== undefined) {
- throw new Error(
- response.data.errors.map((e: { message: string; type: string }) =>
- e.message
- ).join("\n"),
- );
- }
- if (response.data.data !== undefined) {
- return response.data.data.user;
- } else {
- return null;
- }
- } catch (error) {
- console.error(
- `Attempt ${attempt} failed with GITHUB_TOKEN${attempt + 1}:`,
- error,
- );
- }
-
- console.log(`Retrying in ${retryDelay / 1000} seconds...`);
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
- }
-
- throw new Error(`Max retries (${maxRetries}) exceeded.`);
- }
-}
diff --git a/vercel.json b/vercel.json
index 9bceb939..5288cb87 100644
--- a/vercel.json
+++ b/vercel.json
@@ -1,7 +1,14 @@
{
- "builds":[{
- "src": "**/index.ts",
- "use": "vercel-deno@1.1.1"
- }],
- "routes": [{ "src": "/.*", "dest": "/" }]
-}
+ "public": true,
+ "functions": {
+ "api/**/*.[jt]s": {
+ "runtime": "vercel-deno@3.0.4"
+ }
+ },
+ "rewrites": [
+ {
+ "source": "/(.*)",
+ "destination": "/api/$1"
+ }
+ ]
+}
\ No newline at end of file
From 495eba6a751c1d68d9bccb5498db578951f2535d Mon Sep 17 00:00:00 2001
From: Alexandro Castro <10711649+AlexcastroDev@users.noreply.github.com>
Date: Sat, 30 Sep 2023 06:49:18 +0100
Subject: [PATCH 14/33] chore: types improvements (#226)
chore: types improvements
chore: types improvements
---
.github/workflows/testing.yml | 25 +--
.gitignore | 1 +
CONTRIBUTING.md | 15 +-
README.md | 159 +++++++++---------
api/index.ts | 13 +-
deno.json | 9 +
src/.DS_Store | Bin 0 -> 6148 bytes
src/Helpers/Logger.ts | 19 +++
src/Helpers/Retry.ts | 4 +-
src/Helpers/__tests__/Retry.test.ts | 1 -
src/Repository/GithubRepository.ts | 13 +-
src/Services/GithubApiService.ts | 64 ++++---
src/StaticRenderRegeneration/cache_manager.ts | 3 +-
src/StaticRenderRegeneration/index.ts | 49 +++---
src/StaticRenderRegeneration/types.ts | 10 +-
src/StaticRenderRegeneration/utils.ts | 60 +++----
src/Types/EServiceKindError.ts | 4 +
src/Types/Request.ts | 6 +
src/Types/ServiceError.ts | 20 +++
src/Types/index.ts | 4 +-
src/error_page.ts | 5 +
src/icons.ts | 2 +-
src/pages/Error.ts | 23 +++
src/theme.ts | 5 +-
src/trophy.ts | 51 +++---
src/trophy_list.ts | 31 ++--
src/user_info.ts | 11 +-
vercel.json | 26 +--
28 files changed, 387 insertions(+), 246 deletions(-)
create mode 100644 deno.json
create mode 100644 src/.DS_Store
create mode 100644 src/Helpers/Logger.ts
create mode 100644 src/Types/EServiceKindError.ts
create mode 100644 src/Types/ServiceError.ts
create mode 100644 src/pages/Error.ts
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index b8215530..8d42acdc 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -7,16 +7,21 @@ on:
jobs:
install-dependencies:
runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ deno-version: [1.36.1]
+
steps:
- - name: Check if PR is in draft mode
- run: |
- if [ "${{ github.event.pull_request.draft }}" == "true" ]; then
- echo "PR is in draft mode, skipping workflow"
- exit 78
- fi
- - name: Set up Deno
+ - name: Git Checkout Deno Module
+ uses: actions/checkout@v2
+ - name: Use Deno Version ${{ matrix.deno-version }}
uses: denolib/setup-deno@v2
with:
- deno-version: "1.37.0"
- - name: Run tests
- run: deno test --allow-env
+ deno-version: ${{ matrix.deno-version }}
+ - name: Deno format check
+ run: deno fmt --check
+ - name: Deno lint check
+ run: deno task lint
+ - name: Test Deno Module
+ run: deno task test
diff --git a/.gitignore b/.gitignore
index 3c497e35..2aa5d60a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.vscode
.env
.idea
+.lock
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 95eaad16..e8cf04a8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,14 +2,14 @@
## Environment
-* Deno >= v1.9.2
-* [Vercel](https://vercel.com/)
-* GitHub API v4
+- Deno >= v1.9.2
+- [Vercel](https://vercel.com/)
+- GitHub API v4
## Local Run
-Create `.env` file to project root directory, and write your GitHub token to the `.env` file.
-Please select the authority of `repo` when creating token.
+Create `.env` file to project root directory, and write your GitHub token to the
+`.env` file. Please select the authority of `repo` when creating token.
```properties
GITHUB_TOKEN1=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
@@ -23,7 +23,7 @@ GITHUB_API=https://github.example.com/api/graphql
Run local server.
```sh
-deno run --allow-net --allow-read --allow-env --allow-write debug.ts
+deno task start
```
Open localhost from your browser.
@@ -36,7 +36,8 @@ Read the [.editorconfig](./.editorconfig)
## Run deno lint
-If you want to contribute to my project, you should check the lint with the following command.
+If you want to contribute to my project, you should check the lint with the
+following command.
```sh
deno lint --unstable
diff --git a/README.md b/README.md
index b4727c8b..96bc8cfd 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,26 @@
-
+
GitHub Profile Trophy
🏆 Add dynamically generated GitHub Stat Trophies on your readme
-
+
-
-
+
+
-
+
-
+
-
+
@@ -28,13 +28,14 @@
-
+
# Quick Start
-Add the following code to your readme. When pasting the code into your profile's readme, change the `?username=` value to your GitHub's username.
+Add the following code to your readme. When pasting the code into your profile's
+readme, change the `?username=` value to your GitHub's username.
```
[![trophy](https://github-profile-trophy.vercel.app/?username=ryo-ma)](https://github.com/ryo-ma/github-profile-trophy)
@@ -51,6 +52,7 @@ Add optional parameter of the theme.
```
[![trophy](https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=onedark)](https://github.com/ryo-ma/github-profile-trophy)
```
+
@@ -61,22 +63,25 @@ Add optional parameter of the theme.
Ranks are `SSS` `SS` `S` `AAA` `AA` `A` `B` `C` `UNKNOWN` `SECRET`.
-| Rank | Description |
-| ---- | ---- |
-| SSS, SS, S | You are at a hard to reach rank. You can brag. |
-| AAA, AA, A | You will reach this rank if you do your best. Let's aim here first. |
-| B, C | You are currently making good progress. Let's aim a bit higher. |
-| UNKNOWN | You have not taken action yet. Let's act first. |
-| SECRET | This rank is very rare. The trophy will not be displayed until certain conditions are met. |
+| Rank | Description |
+| ---------- | ------------------------------------------------------------------------------------------ |
+| SSS, SS, S | You are at a hard to reach rank. You can brag. |
+| AAA, AA, A | You will reach this rank if you do your best. Let's aim here first. |
+| B, C | You are currently making good progress. Let's aim a bit higher. |
+| UNKNOWN | You have not taken action yet. Let's act first. |
+| SECRET | This rank is very rare. The trophy will not be displayed until certain conditions are met. |
## Secret Rank
-The acquisition condition is secret, but you can know the condition by reading this code.
+
+The acquisition condition is secret, but you can know the condition by reading
+this code.
-There are only a few secret trophies. Therefore, if you come up with interesting conditions, I will consider adding a trophy. I am waiting for contributions.
+There are only a few secret trophies. Therefore, if you come up with interesting
+conditions, I will consider adding a trophy. I am waiting for contributions.
# About Display details
@@ -90,23 +95,21 @@ There are only a few secret trophies. Therefore, if you come up with interesting
4. Target aggregation result.
5. Next Rank Bar. The road from the current rank to the next rank.
-
# Optional Request Parameters
-* [title](#filter-by-titles)
-* [rank](#filter-by-ranks)
-* [column](#specify-the-maximum-row--column-size)
-* [row](#specify-the-maximum-row--column-size)
-* [theme](#apply-theme)
-* [margin-w](#margin-width)
-* [margin-h](#margin-height)
-* [no-bg](#transparent-background)
-* [no-frame](#hide-frames)
-
+- [title](#filter-by-titles)
+- [rank](#filter-by-ranks)
+- [column](#specify-the-maximum-row--column-size)
+- [row](#specify-the-maximum-row--column-size)
+- [theme](#apply-theme)
+- [margin-w](#margin-width)
+- [margin-h](#margin-height)
+- [no-bg](#transparent-background)
+- [no-frame](#hide-frames)
## Filter by titles
-You can filter the display by specifying the titles of trophy.
+You can filter the display by specifying the titles of trophy.
```
https://github-profile-trophy.vercel.app/?username=ryo-ma&title=Followers
@@ -124,12 +127,13 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&title=Stars,Followers
## Filter by ranks
-You can filter the display by specifying the ranks.
+You can filter the display by specifying the ranks.\
`Available values: SECRET SSS SS S AAA AA A B C`
```
https://github-profile-trophy.vercel.app/?username=ryo-ma&rank=S
```
+
@@ -146,26 +150,28 @@ You can also exclude ranks.
https://github-profile-trophy.vercel.app/?username=ryo-ma&rank=-C,-B
```
-
## Specify the maximum row & column size
-You can specify the maximum row and column size.
+You can specify the maximum row and column size.\
Trophy will be hidden if it exceeds the range of both row and column.
-`Available value: number type`
+`Available value: number type`\
`Default: column=6 row=3`
Restrict only row
+
```
https://github-profile-trophy.vercel.app/?username=ryo-ma&row=2
```
Restrict only column
+
```
https://github-profile-trophy.vercel.app/?username=ryo-ma&column=2
```
Restrict row & column
+
```
https://github-profile-trophy.vercel.app/?username=ryo-ma&row=2&column=3
```
@@ -175,47 +181,49 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&row=2&column=3
Adaptive column
+
```
https://github-profile-trophy.vercel.app/?username=ryo-ma&column=-1
```
-You can set `columns` to `-1` to adapt the width to the number of trophies, the parameter `row` will be ignored.
+You can set `columns` to `-1` to adapt the width to the number of trophies, the
+parameter `row` will be ignored.
## Apply theme
Available themes.
-| theme |
-| ---- |
-| [flat](#flat) |
-| [onedark](#onedark) |
-| [gruvbox](#gruvbox) |
-| [dracula](#dracula) |
-| [monokai](#monokai) |
-| [chalk](#chalk) |
-| [nord](#nord) |
-| [alduin](#alduin) |
-| [darkhub](#darkhub) |
-| [juicyfresh](#juicyfresh) |
-| [buddhism](#buddhism) |
-| [oldie](#oldie) |
-| [radical](#radical) |
-| [onestar](#onestar) |
-| [discord](#discord) |
-| [algolia](#algolia) |
-| [gitdimmed](#gitdimmed) |
-| [tokyonight](#tokyonight) |
-| [matrix](#matrix) |
-| [apprentice](#apprentice) |
+| theme |
+| --------------------------- |
+| [flat](#flat) |
+| [onedark](#onedark) |
+| [gruvbox](#gruvbox) |
+| [dracula](#dracula) |
+| [monokai](#monokai) |
+| [chalk](#chalk) |
+| [nord](#nord) |
+| [alduin](#alduin) |
+| [darkhub](#darkhub) |
+| [juicyfresh](#juicyfresh) |
+| [buddhism](#buddhism) |
+| [oldie](#oldie) |
+| [radical](#radical) |
+| [onestar](#onestar) |
+| [discord](#discord) |
+| [algolia](#algolia) |
+| [gitdimmed](#gitdimmed) |
+| [tokyonight](#tokyonight) |
+| [matrix](#matrix) |
+| [apprentice](#apprentice) |
| [dark_dimmed](#dark_dimmed) |
-| [dark_lover](#dark_lover) |
-
+| [dark_lover](#dark_lover) |
### flat
```
https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=flat
```
+
@@ -280,7 +288,6 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=nord
-
### alduin
```
@@ -388,7 +395,7 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=tokyonight
```
-
+
### matrix
@@ -398,7 +405,7 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=matrix
```
-
+
### apprentice
@@ -408,7 +415,7 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=apprentice
```
-
+
### dark_dimmed
@@ -418,7 +425,7 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=dark_dimmed
```
-
+
### dark_lover
@@ -428,13 +435,13 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&theme=dark_lover
```
-
+
## Margin Width
-You can put a margin in the width between trophies.
-`Available value: number type`
+You can put a margin in the width between trophies.\
+`Available value: number type`\
`Default: margin-w=0`
```
@@ -447,8 +454,8 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&margin-w=15
## Margin Height
-You can put a margin in the height between trophies.
-`Available value: number type`
+You can put a margin in the height between trophies.\
+`Available value: number type`\
`Default: margin-h=0`
```
@@ -467,8 +474,8 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&column=3&margin-w=15&m
## Transparent background
-You can turn the background transparent.
-`Available value: boolean type (true or false)`
+You can turn the background transparent.\
+`Available value: boolean type (true or false)`\
`Default: no-bg=false`
```
@@ -479,12 +486,10 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&no-bg=true
-
-
## Hide frames
-You can hide the frames around the trophies.
-`Available value: boolean type (true or false)`
+You can hide the frames around the trophies.\
+`Available value: boolean type (true or false)`\
`Default: no-frame=false`
```
@@ -495,12 +500,12 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&no-frame=true
-
# Contribution Guide
+
Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
# Testing
```bash
-deno test --allow-env
+deno task test
```
diff --git a/api/index.ts b/api/index.ts
index 4a095c78..68d5215a 100644
--- a/api/index.ts
+++ b/api/index.ts
@@ -1,11 +1,13 @@
import { Card } from "../src/card.ts";
import { CONSTANTS, parseParams } from "../src/utils.ts";
import { COLORS, Theme } from "../src/theme.ts";
-import { Error400, Error404 } from "../src/error_page.ts";
+import { Error400 } from "../src/error_page.ts";
import "https://deno.land/x/dotenv@v0.5.0/load.ts";
import { staticRenderRegeneration } from "../src/StaticRenderRegeneration/index.ts";
import { GithubRepositoryService } from "../src/Repository/GithubRepository.ts";
import { GithubApiService } from "../src/Services/GithubApiService.ts";
+import { ServiceError } from "../src/Types/index.ts";
+import { ErrorPage } from "../src/pages/Error.ts";
const serviceProvider = new GithubApiService();
const client = new GithubRepositoryService(serviceProvider).repository;
@@ -74,14 +76,11 @@ async function app(req: Request): Promise {
);
}
const userInfo = await client.requestUserInfo(username);
- if (userInfo === null) {
- const error = new Error404(
- "Can not find a user with username: " + username,
- );
+ if (userInfo instanceof ServiceError) {
return new Response(
- error.render(),
+ ErrorPage({ error: userInfo, username }).render(),
{
- status: error.status,
+ status: userInfo.code,
headers: new Headers({ "Content-Type": "text" }),
},
);
diff --git a/deno.json b/deno.json
new file mode 100644
index 00000000..327d1ec1
--- /dev/null
+++ b/deno.json
@@ -0,0 +1,9 @@
+{
+ "tasks": {
+ "start": "deno run -A debug.ts",
+ "debug": "deno --inspect-brk -A debug.ts",
+ "format": "deno fmt",
+ "lint": "deno lint",
+ "test": "ENV_TYPE=test deno test --allow-env"
+ }
+}
diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..dbcdcabe06f6c0b9f79fd341be39ab7c161065a8
GIT binary patch
literal 6148
zcmeH~%}T>i5QWcZ7X>$6c6ndHHwdM^fO!GMmLfYKm=w9c<-ex?^3mjfCz}d4*~x^G`ee7
zI;O^_gG-D6)CI#~ypCCd+B`t*O2<@2XqKhaEVUXjEXx^hmDiPysaX!I;lt|5RuhWF
z(|LZ2bXb?FRRl!fn!s&tSKj|0=)cVW*G1ZifC&660=C%p+YMi-dh6unyw^7R6aCwm
o8|hr46;q=XbK|XeeUaDvn)kcXF*V8=k8-Mh1e}XZ1pb1+7jJhOK>z>%
literal 0
HcmV?d00001
diff --git a/src/Helpers/Logger.ts b/src/Helpers/Logger.ts
new file mode 100644
index 00000000..896192a5
--- /dev/null
+++ b/src/Helpers/Logger.ts
@@ -0,0 +1,19 @@
+const enableLogging = Deno.env.get("ENV_TYPE") !== "test";
+
+export class Logger {
+ public static log(message: unknown): void {
+ if (!enableLogging) return;
+ console.log(message);
+ }
+
+ public static error(message: unknown): void {
+ if (!enableLogging) return;
+
+ console.error(message);
+ }
+ public static warn(message: unknown): void {
+ if (!enableLogging) return;
+
+ console.warn(message);
+ }
+}
diff --git a/src/Helpers/Retry.ts b/src/Helpers/Retry.ts
index be1eb9fc..a7ce9a95 100644
--- a/src/Helpers/Retry.ts
+++ b/src/Helpers/Retry.ts
@@ -1,3 +1,5 @@
+import { Logger } from "./Logger.ts";
+
export type RetryCallbackProps = {
attempt: number;
};
@@ -16,7 +18,7 @@ async function* createAsyncIterable(
return;
} catch (e) {
yield null;
- console.error(e);
+ Logger.error(e);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
diff --git a/src/Helpers/__tests__/Retry.test.ts b/src/Helpers/__tests__/Retry.test.ts
index 7fcad13e..a1fec42b 100644
--- a/src/Helpers/__tests__/Retry.test.ts
+++ b/src/Helpers/__tests__/Retry.test.ts
@@ -44,7 +44,6 @@ Deno.test("Should retry", async () => {
Deno.test("Should retry the asyncronous callback", async () => {
let countErrors = 0;
-
const callbackError = async () => {
countErrors++;
// Mock request in callback
diff --git a/src/Repository/GithubRepository.ts b/src/Repository/GithubRepository.ts
index b345fd35..c2a408c6 100644
--- a/src/Repository/GithubRepository.ts
+++ b/src/Repository/GithubRepository.ts
@@ -1,3 +1,4 @@
+import { ServiceError } from "../Types/index.ts";
import {
GitHubUserActivity,
GitHubUserIssue,
@@ -7,17 +8,19 @@ import {
} from "../user_info.ts";
export abstract class GithubRepository {
- abstract requestUserInfo(username: string): Promise;
+ abstract requestUserInfo(username: string): Promise;
abstract requestUserActivity(
username: string,
- ): Promise;
- abstract requestUserIssue(username: string): Promise;
+ ): Promise;
+ abstract requestUserIssue(
+ username: string,
+ ): Promise;
abstract requestUserPullRequest(
username: string,
- ): Promise;
+ ): Promise;
abstract requestUserRepository(
username: string,
- ): Promise;
+ ): Promise;
}
export class GithubRepositoryService {
diff --git a/src/Services/GithubApiService.ts b/src/Services/GithubApiService.ts
index 4d7a9ccd..685bac58 100644
--- a/src/Services/GithubApiService.ts
+++ b/src/Services/GithubApiService.ts
@@ -14,8 +14,11 @@ import {
} from "../Schemas/index.ts";
import { soxa } from "../../deps.ts";
import { Retry } from "../Helpers/Retry.ts";
-import { QueryDefaultResponse } from "../Types/index.ts";
+import { GithubError, QueryDefaultResponse } from "../Types/index.ts";
import { CONSTANTS } from "../utils.ts";
+import { EServiceKindError } from "../Types/EServiceKindError.ts";
+import { ServiceError } from "../Types/ServiceError.ts";
+import { Logger } from "../Helpers/Logger.ts";
// Need to be here - Exporting from another file makes array of null
export const TOKENS = [
@@ -26,35 +29,40 @@ export const TOKENS = [
export class GithubApiService extends GithubRepository {
async requestUserRepository(
username: string,
- ): Promise {
+ ): Promise {
return await this.executeQuery(queryUserRepository, {
username,
});
}
async requestUserActivity(
username: string,
- ): Promise {
+ ): Promise {
return await this.executeQuery(queryUserActivity, {
username,
});
}
- async requestUserIssue(username: string): Promise {
+ async requestUserIssue(
+ username: string,
+ ): Promise {
return await this.executeQuery(queryUserIssue, {
username,
});
}
async requestUserPullRequest(
username: string,
- ): Promise {
+ ): Promise {
return await this.executeQuery(
queryUserPullRequest,
{ username },
);
}
- async requestUserInfo(username: string): Promise {
+ async requestUserInfo(username: string): Promise {
// Avoid to call others if one of them is null
const repository = await this.requestUserRepository(username);
- if (repository === null) return null;
+ if (repository instanceof ServiceError) {
+ Logger.error(repository);
+ return repository;
+ }
const promises = Promise.allSettled([
this.requestUserActivity(username),
@@ -69,15 +77,34 @@ export class GithubApiService extends GithubRepository {
];
if (status.includes("rejected")) {
- console.error(`Can not find a user with username:' ${username}'`);
- return null;
+ Logger.error(`Can not find a user with username:' ${username}'`);
+ return new ServiceError("not found", EServiceKindError.NOT_FOUND);
}
return new UserInfo(
(activity as PromiseFulfilledResult).value,
(issue as PromiseFulfilledResult).value,
(pullRequest as PromiseFulfilledResult).value,
- repository,
+ repository as GitHubUserRepository,
+ );
+ }
+
+ private handleError(responseErrors: GithubError[]): ServiceError {
+ const errors = responseErrors ?? [];
+ const isRateLimitExceeded = (errors ?? []).some((error) => {
+ error.type.includes(EServiceKindError.RATE_LIMIT);
+ });
+
+ if (isRateLimitExceeded) {
+ throw new ServiceError(
+ "Rate limit exceeded",
+ EServiceKindError.RATE_LIMIT,
+ );
+ }
+
+ throw new ServiceError(
+ "unknown error",
+ EServiceKindError.NOT_FOUND,
);
}
@@ -97,24 +124,23 @@ export class GithubApiService extends GithubRepository {
Authorization: `bearer ${TOKENS[attempt]}`,
},
});
- }) as QueryDefaultResponse<{ user: T; errors?: unknown[] }>;
+ }) as QueryDefaultResponse<{ user: T }>;
- if (response.data.data.errors) {
- throw new Error("Error from Github API", {
- cause: response.data.data.errors,
- });
+ if (response?.data?.errors) {
+ return this.handleError(response?.data?.errors);
}
- return response?.data?.data?.user ?? null;
+ return response?.data?.data?.user ??
+ new ServiceError("not found", EServiceKindError.NOT_FOUND);
} catch (error) {
// TODO: Move this to a logger instance later
if (error instanceof Error && error.cause) {
- console.error(JSON.stringify(error.cause, null, 2));
+ Logger.error(JSON.stringify(error.cause, null, 2));
} else {
- console.error(error);
+ Logger.error(error);
}
- return null;
+ return new ServiceError("not found", EServiceKindError.NOT_FOUND);
}
}
}
diff --git a/src/StaticRenderRegeneration/cache_manager.ts b/src/StaticRenderRegeneration/cache_manager.ts
index 2f11479c..f0a3a87c 100644
--- a/src/StaticRenderRegeneration/cache_manager.ts
+++ b/src/StaticRenderRegeneration/cache_manager.ts
@@ -1,3 +1,4 @@
+import { Logger } from "../Helpers/Logger.ts";
import { existsSync } from "./utils.ts";
export class CacheManager {
@@ -44,7 +45,7 @@ export class CacheManager {
const data = new TextEncoder().encode(text);
Deno.writeFile(this.cacheFilePath, data, { create: true }).catch(() => {
- console.warn("Failed to save cache file");
+ Logger.warn("Failed to save cache file");
});
}
}
diff --git a/src/StaticRenderRegeneration/index.ts b/src/StaticRenderRegeneration/index.ts
index d93fc61f..bc846213 100644
--- a/src/StaticRenderRegeneration/index.ts
+++ b/src/StaticRenderRegeneration/index.ts
@@ -1,29 +1,36 @@
import { CacheManager } from "./cache_manager.ts";
import { StaticRegenerationOptions } from "./types.ts";
-import { getUrl, readCache, generateUUID } from "./utils.ts";
+import { getUrl, hashString, readCache } from "./utils.ts";
-export async function staticRenderRegeneration(request: Request, options: StaticRegenerationOptions, render: (request: Request) => Promise) {
- // avoid TypeError: Invalid URL at deno:core
- const url = getUrl(request)
+export async function staticRenderRegeneration(
+ request: Request,
+ options: StaticRegenerationOptions,
+ render: (request: Request) => Promise,
+) {
+ // avoid TypeError: Invalid URL at deno:core
+ const url = getUrl(request);
- // if more conditions are added, make sure to create a variable to skipCache
- if (url.pathname === "/favicon.ico") {
- return await render(request);
- }
+ // if more conditions are added, make sure to create a variable to skipCache
+ if (url.pathname === "/favicon.ico") {
+ return await render(request);
+ }
- const cacheFile = await generateUUID(url.pathname + (url.search ?? ""));
- const cacheManager = new CacheManager(options.revalidate ?? 0, cacheFile);
- if (cacheManager.isCacheValid) {
- const cache = readCache(cacheManager.cacheFilePath)
- if(cache !== null) {
- return new Response(cache, {
- headers: options.headers ?? new Headers({}),
- });
- }
+ const cacheFile = await hashString(url.pathname + (url.search ?? ""));
+ const cacheManager = new CacheManager(options.revalidate ?? 0, cacheFile);
+ if (cacheManager.isCacheValid) {
+ const cache = readCache(cacheManager.cacheFilePath);
+ if (cache !== null) {
+ return new Response(cache, {
+ headers: options.headers ?? new Headers({}),
+ });
}
-
- const response = await render(request)
- cacheManager.save(response)
+ }
+
+ const response = await render(request);
+
+ if (response.status >= 200 && response.status < 300) {
+ cacheManager.save(response);
+ }
- return response
+ return response;
}
diff --git a/src/StaticRenderRegeneration/types.ts b/src/StaticRenderRegeneration/types.ts
index e9b79076..c98b66c7 100644
--- a/src/StaticRenderRegeneration/types.ts
+++ b/src/StaticRenderRegeneration/types.ts
@@ -1,6 +1,6 @@
export interface StaticRegenerationOptions {
- // The number of milliseconds before the page should be revalidated
- revalidate?: number
- // The headers to be sent with the response
- headers?: Headers
-}
\ No newline at end of file
+ // The number of milliseconds before the page should be revalidated
+ revalidate?: number;
+ // The headers to be sent with the response
+ headers?: Headers;
+}
diff --git a/src/StaticRenderRegeneration/utils.ts b/src/StaticRenderRegeneration/utils.ts
index ebc19c4b..0c04cc5c 100644
--- a/src/StaticRenderRegeneration/utils.ts
+++ b/src/StaticRenderRegeneration/utils.ts
@@ -1,42 +1,42 @@
export function getUrl(request: Request) {
- try {
- return new URL(request.url)
- } catch {
- return {
- pathname: request.url,
- search: request.url
- }
- }
+ try {
+ return new URL(request.url);
+ } catch {
+ return {
+ pathname: request.url,
+ search: request.url,
+ };
+ }
}
export function readCache(cacheFilePath: string): Uint8Array | null {
- try {
- return Deno.readFileSync(cacheFilePath)
- } catch {
- return null
- }
+ try {
+ return Deno.readFileSync(cacheFilePath);
+ } catch {
+ return null;
+ }
}
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
-export async function generateUUID(message: string): Promise {
- const encoder = new TextEncoder();
- const data = encoder.encode(message);
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
-
- const hashArray = Array.from(new Uint8Array(hashBuffer));
- const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+export async function hashString(message: string): Promise {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(message);
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
- return hashHex;
-}
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(
+ "",
+ );
+ return hashHex;
+}
export const existsSync = (filename: string): boolean => {
- try {
- Deno.statSync(filename);
- // successful, file or directory must exist
- return true;
- } catch {
- return false;
- }
+ try {
+ Deno.statSync(filename);
+ // successful, file or directory must exist
+ return true;
+ } catch {
+ return false;
+ }
};
-
\ No newline at end of file
diff --git a/src/Types/EServiceKindError.ts b/src/Types/EServiceKindError.ts
new file mode 100644
index 00000000..172ed05f
--- /dev/null
+++ b/src/Types/EServiceKindError.ts
@@ -0,0 +1,4 @@
+export const enum EServiceKindError {
+ RATE_LIMIT = "RATE_LIMITED",
+ NOT_FOUND = "NOT_FOUND",
+}
diff --git a/src/Types/Request.ts b/src/Types/Request.ts
index 5ef517e7..85f1362c 100644
--- a/src/Types/Request.ts
+++ b/src/Types/Request.ts
@@ -1,5 +1,11 @@
+export type GithubError = {
+ message: string;
+ type: string;
+};
+
export type QueryDefaultResponse = {
data: {
data: T;
+ errors: GithubError[];
};
};
diff --git a/src/Types/ServiceError.ts b/src/Types/ServiceError.ts
new file mode 100644
index 00000000..8a0db072
--- /dev/null
+++ b/src/Types/ServiceError.ts
@@ -0,0 +1,20 @@
+import { EServiceKindError } from "./EServiceKindError.ts";
+
+export class ServiceError extends Error {
+ constructor(message: string, kind: EServiceKindError) {
+ super(message);
+ this.name = "ServiceError";
+ this.cause = kind;
+ }
+
+ get code(): number {
+ switch (this.cause) {
+ case EServiceKindError.RATE_LIMIT:
+ return 419;
+ case EServiceKindError.NOT_FOUND:
+ return 404;
+ default:
+ return 400;
+ }
+ }
+}
diff --git a/src/Types/index.ts b/src/Types/index.ts
index 07f4ba9c..0e52b91c 100644
--- a/src/Types/index.ts
+++ b/src/Types/index.ts
@@ -1 +1,3 @@
-export * from './Request.ts'
\ No newline at end of file
+export * from "./Request.ts";
+export * from "./ServiceError.ts";
+export * from "./EServiceKindError.ts";
diff --git a/src/error_page.ts b/src/error_page.ts
index 0f3076a4..160bba49 100644
--- a/src/error_page.ts
+++ b/src/error_page.ts
@@ -14,6 +14,11 @@ export class Error400 extends BaseError {
readonly message = "Bad Request";
}
+export class Error419 extends BaseError {
+ readonly status = 419;
+ readonly message = "Rate Limit Exceeded";
+}
+
export class Error404 extends BaseError {
readonly status = 404;
readonly message = "Not Found";
diff --git a/src/icons.ts b/src/icons.ts
index 8e7dbf7f..7cabc097 100644
--- a/src/icons.ts
+++ b/src/icons.ts
@@ -2,7 +2,7 @@ import { RANK } from "./utils.ts";
import { Theme } from "./theme.ts";
const leafIcon = (laurel: string): string => {
- return `
+ return `
Created by potrace 1.15, written by Peter Selinger 2001-2017
diff --git a/src/pages/Error.ts b/src/pages/Error.ts
new file mode 100644
index 00000000..3e93d3dd
--- /dev/null
+++ b/src/pages/Error.ts
@@ -0,0 +1,23 @@
+import { EServiceKindError, ServiceError } from "../Types/index.ts";
+import { Error400, Error404, Error419 } from "../error_page.ts";
+
+interface ErrorPageProps {
+ error: ServiceError;
+ username: string;
+}
+
+export function ErrorPage({ error, username }: ErrorPageProps) {
+ let cause: Error400 | Error404 | Error419 = new Error400();
+
+ if (error.cause === EServiceKindError.RATE_LIMIT) {
+ cause = new Error419();
+ }
+
+ if (error.cause === EServiceKindError.NOT_FOUND) {
+ cause = new Error404(
+ "Can not find a user with username: " + username,
+ );
+ }
+
+ return cause;
+}
diff --git a/src/theme.ts b/src/theme.ts
index 04c4ac07..977938dd 100644
--- a/src/theme.ts
+++ b/src/theme.ts
@@ -1,4 +1,4 @@
-export const COLORS: {[name: string]: Theme} = {
+export const COLORS: { [name: string]: Theme } = {
default: {
BACKGROUND: "#FFF",
TITLE: "#000",
@@ -550,7 +550,7 @@ export const COLORS: {[name: string]: Theme} = {
DEFAULT_RANK_BASE: "#7f6ceb",
DEFAULT_RANK_SHADOW: "#a598ed",
DEFAULT_RANK_TEXT: "#7f6ceb",
- }
+ },
};
export interface Theme {
@@ -577,4 +577,3 @@ export interface Theme {
DEFAULT_RANK_SHADOW: string;
DEFAULT_RANK_TEXT: string;
}
-
diff --git a/src/trophy.ts b/src/trophy.ts
index 66d9f94f..778f30d1 100644
--- a/src/trophy.ts
+++ b/src/trophy.ts
@@ -1,5 +1,5 @@
-import { getTrophyIcon, getNextRankBar } from "./icons.ts";
-import { CONSTANTS, RANK, abridgeScore, RANK_ORDER } from "./utils.ts";
+import { getNextRankBar, getTrophyIcon } from "./icons.ts";
+import { abridgeScore, CONSTANTS, RANK, RANK_ORDER } from "./utils.ts";
import { Theme } from "./theme.ts";
class RankCondition {
@@ -10,7 +10,6 @@ class RankCondition {
) {}
}
-
export class Trophy {
rankCondition: RankCondition | null = null;
rank: RANK = RANK.UNKNOWN;
@@ -59,14 +58,16 @@ export class Trophy {
const result = progress / distance;
return result;
}
- render(theme: Theme,
+ render(
+ theme: Theme,
x = 0,
y = 0,
panelSize = CONSTANTS.DEFAULT_PANEL_SIZE,
noBackground = CONSTANTS.DEFAULT_NO_BACKGROUND,
noFrame = CONSTANTS.DEFAULT_NO_FRAME,
): string {
- const { BACKGROUND: PRIMARY, TITLE: SECONDARY, TEXT, NEXT_RANK_BAR } = theme;
+ const { BACKGROUND: PRIMARY, TITLE: SECONDARY, TEXT, NEXT_RANK_BAR } =
+ theme;
const nextRankBar = getNextRankBar(
this.title,
this.calculateNextRankPercentage(),
@@ -90,8 +91,8 @@ export class Trophy {
height="${panelSize - 1}"
stroke="#e1e4e8"
fill="${PRIMARY}"
- stroke-opacity="${noFrame ? '0' : '1'}"
- fill-opacity="${noBackground ? '0' : '1'}"
+ stroke-opacity="${noFrame ? "0" : "1"}"
+ fill-opacity="${noBackground ? "0" : "1"}"
/>
${getTrophyIcon(theme, this.rank)}
${this.title}
@@ -103,8 +104,8 @@ export class Trophy {
}
}
-export class MultipleLangTrophy extends Trophy{
- constructor(score: number){
+export class MultipleLangTrophy extends Trophy {
+ constructor(score: number) {
const rankConditions = [
new RankCondition(
RANK.SECRET,
@@ -119,8 +120,8 @@ export class MultipleLangTrophy extends Trophy{
}
}
-export class AllSuperRankTrophy extends Trophy{
- constructor(score: number){
+export class AllSuperRankTrophy extends Trophy {
+ constructor(score: number) {
const rankConditions = [
new RankCondition(
RANK.SECRET,
@@ -131,12 +132,12 @@ export class AllSuperRankTrophy extends Trophy{
super(score, rankConditions);
this.title = "AllSuperRank";
this.filterTitles = ["AllSuperRank"];
- this.bottomMessage = "All S Rank"
+ this.bottomMessage = "All S Rank";
this.hidden = true;
}
}
-export class Joined2020Trophy extends Trophy{
- constructor(score: number){
+export class Joined2020Trophy extends Trophy {
+ constructor(score: number) {
const rankConditions = [
new RankCondition(
RANK.SECRET,
@@ -147,12 +148,12 @@ export class Joined2020Trophy extends Trophy{
super(score, rankConditions);
this.title = "Joined2020";
this.filterTitles = ["Joined2020"];
- this.bottomMessage = "Joined 2020"
+ this.bottomMessage = "Joined 2020";
this.hidden = true;
}
}
-export class AncientAccountTrophy extends Trophy{
- constructor(score: number){
+export class AncientAccountTrophy extends Trophy {
+ constructor(score: number) {
const rankConditions = [
new RankCondition(
RANK.SECRET,
@@ -163,12 +164,12 @@ export class AncientAccountTrophy extends Trophy{
super(score, rankConditions);
this.title = "AncientUser";
this.filterTitles = ["AncientUser"];
- this.bottomMessage = "Before 2010"
+ this.bottomMessage = "Before 2010";
this.hidden = true;
}
}
-export class LongTimeAccountTrophy extends Trophy{
- constructor(score: number){
+export class LongTimeAccountTrophy extends Trophy {
+ constructor(score: number) {
const rankConditions = [
new RankCondition(
RANK.SECRET,
@@ -182,8 +183,8 @@ export class LongTimeAccountTrophy extends Trophy{
this.hidden = true;
}
}
-export class MultipleOrganizationsTrophy extends Trophy{
- constructor(score: number){
+export class MultipleOrganizationsTrophy extends Trophy {
+ constructor(score: number) {
const rankConditions = [
new RankCondition(
RANK.SECRET,
@@ -199,8 +200,8 @@ export class MultipleOrganizationsTrophy extends Trophy{
}
}
-export class OGAccountTrophy extends Trophy{
- constructor(score: number){
+export class OGAccountTrophy extends Trophy {
+ constructor(score: number) {
const rankConditions = [
new RankCondition(
RANK.SECRET,
@@ -211,7 +212,7 @@ export class OGAccountTrophy extends Trophy{
super(score, rankConditions);
this.title = "OGUser";
this.filterTitles = ["OGUser"];
- this.bottomMessage = "Joined 2008"
+ this.bottomMessage = "Joined 2008";
this.hidden = true;
}
}
diff --git a/src/trophy_list.ts b/src/trophy_list.ts
index 7b5b9c51..d8e406f3 100644
--- a/src/trophy_list.ts
+++ b/src/trophy_list.ts
@@ -1,22 +1,22 @@
import {
- Trophy,
- TotalStarTrophy,
+ AllSuperRankTrophy,
+ AncientAccountTrophy,
+ Joined2020Trophy,
+ LongTimeAccountTrophy,
+ MultipleLangTrophy,
+ MultipleOrganizationsTrophy,
+ OGAccountTrophy,
TotalCommitTrophy,
TotalFollowerTrophy,
TotalIssueTrophy,
TotalPullRequestTrophy,
TotalRepositoryTrophy,
TotalReviewsTrophy,
- MultipleLangTrophy,
- LongTimeAccountTrophy,
- AncientAccountTrophy,
- OGAccountTrophy,
- Joined2020Trophy,
- AllSuperRankTrophy,
- MultipleOrganizationsTrophy,
+ TotalStarTrophy,
+ Trophy,
} from "./trophy.ts";
import { UserInfo } from "./user_info.ts";
-import { RANK_ORDER, RANK } from "./utils.ts";
+import { RANK, RANK_ORDER } from "./utils.ts";
export class TrophyList {
private trophies = new Array();
@@ -49,7 +49,9 @@ export class TrophyList {
return this.trophies;
}
private get isAllSRank() {
- return this.trophies.every((trophy) => trophy.rank.slice(0, 1) == RANK.S) ? 1 : 0;
+ return this.trophies.every((trophy) => trophy.rank.slice(0, 1) == RANK.S)
+ ? 1
+ : 0;
}
filterByHideen() {
this.trophies = this.trophies.filter((trophy) =>
@@ -64,9 +66,9 @@ export class TrophyList {
filterByRanks(ranks: Array) {
if (ranks.filter((rank) => rank.includes("-")).length !== 0) {
this.trophies = this.trophies.filter((trophy) =>
- !ranks.map(rank => rank.substring(1)).includes(trophy.rank)
- )
- return
+ !ranks.map((rank) => rank.substring(1)).includes(trophy.rank)
+ );
+ return;
}
this.trophies = this.trophies.filter((trophy) =>
ranks.includes(trophy.rank)
@@ -78,4 +80,3 @@ export class TrophyList {
);
}
}
-
diff --git a/src/user_info.ts b/src/user_info.ts
index c2e47c3b..7bfdac88 100644
--- a/src/user_info.ts
+++ b/src/user_info.ts
@@ -88,15 +88,18 @@ export class UserInfo {
const joined2020 = new Date(userActivity.createdAt).getFullYear() == 2020
? 1
: 0;
- const ogAccount =
- new Date(userActivity.createdAt).getFullYear() <= 2008 ? 1 : 0;
+ const ogAccount = new Date(userActivity.createdAt).getFullYear() <= 2008
+ ? 1
+ : 0;
this.totalCommits = totalCommits;
this.totalFollowers = userActivity.followers.totalCount;
- this.totalIssues = userIssue.openIssues.totalCount + userIssue.closedIssues.totalCount;
+ this.totalIssues = userIssue.openIssues.totalCount +
+ userIssue.closedIssues.totalCount;
this.totalOrganizations = userActivity.organizations.totalCount;
this.totalPullRequests = userPullRequest.pullRequests.totalCount;
- this.totalReviews = userActivity.contributionsCollection.totalPullRequestReviewContributions;
+ this.totalReviews =
+ userActivity.contributionsCollection.totalPullRequestReviewContributions;
this.totalStargazers = totalStargazers;
this.totalRepositories = userRepository.repositories.totalCount;
this.languageCount = languages.size;
diff --git a/vercel.json b/vercel.json
index 5288cb87..64706355 100644
--- a/vercel.json
+++ b/vercel.json
@@ -1,14 +1,14 @@
{
- "public": true,
- "functions": {
- "api/**/*.[jt]s": {
- "runtime": "vercel-deno@3.0.4"
- }
- },
- "rewrites": [
- {
- "source": "/(.*)",
- "destination": "/api/$1"
- }
- ]
-}
\ No newline at end of file
+ "public": true,
+ "functions": {
+ "api/**/*.[jt]s": {
+ "runtime": "vercel-deno@3.0.4"
+ }
+ },
+ "rewrites": [
+ {
+ "source": "/(.*)",
+ "destination": "/api/$1"
+ }
+ ]
+}
From c8bbf60527d225bfec4ac17ec7d2567e83bdc59f Mon Sep 17 00:00:00 2001
From: Alex Oliveira
Date: Sat, 30 Sep 2023 09:14:43 +0100
Subject: [PATCH 15/33] fix: task run
---
deno.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/deno.json b/deno.json
index 327d1ec1..2212a01b 100644
--- a/deno.json
+++ b/deno.json
@@ -1,7 +1,7 @@
{
"tasks": {
"start": "deno run -A debug.ts",
- "debug": "deno --inspect-brk -A debug.ts",
+ "debug": "deno run --inspect-brk -A debug.ts",
"format": "deno fmt",
"lint": "deno lint",
"test": "ENV_TYPE=test deno test --allow-env"
From af753bf270ca148685814803263f0093a7256c23 Mon Sep 17 00:00:00 2001
From: Alexandro Castro <10711649+AlexcastroDev@users.noreply.github.com>
Date: Sat, 30 Sep 2023 10:15:58 +0100
Subject: [PATCH 16/33] fix: check of rate limit (#229)
* fix: task run
* fix: check of rate limit
---
.gitignore | 1 +
src/Services/GithubApiService.ts | 12 +++++++++---
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index 2aa5d60a..91b922d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
.env
.idea
.lock
+*.sh
diff --git a/src/Services/GithubApiService.ts b/src/Services/GithubApiService.ts
index 685bac58..96627c1a 100644
--- a/src/Services/GithubApiService.ts
+++ b/src/Services/GithubApiService.ts
@@ -91,9 +91,11 @@ export class GithubApiService extends GithubRepository {
private handleError(responseErrors: GithubError[]): ServiceError {
const errors = responseErrors ?? [];
- const isRateLimitExceeded = (errors ?? []).some((error) => {
- error.type.includes(EServiceKindError.RATE_LIMIT);
- });
+
+ const isRateLimitExceeded = errors.some((error) =>
+ error.type.includes(EServiceKindError.RATE_LIMIT) ||
+ error.message.includes("rate limit")
+ );
if (isRateLimitExceeded) {
throw new ServiceError(
@@ -133,6 +135,10 @@ export class GithubApiService extends GithubRepository {
return response?.data?.data?.user ??
new ServiceError("not found", EServiceKindError.NOT_FOUND);
} catch (error) {
+ if (error instanceof ServiceError) {
+ Logger.error(error);
+ return error;
+ }
// TODO: Move this to a logger instance later
if (error instanceof Error && error.cause) {
Logger.error(JSON.stringify(error.cause, null, 2));
From b4aa73dea1f9b6a452d91498a3de4a5c5d03cff0 Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Sun, 1 Oct 2023 11:49:55 +0900
Subject: [PATCH 17/33] fix: Temporary fix for retry bug
---
src/Services/GithubApiService.ts | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/Services/GithubApiService.ts b/src/Services/GithubApiService.ts
index 96627c1a..8cdfd6c6 100644
--- a/src/Services/GithubApiService.ts
+++ b/src/Services/GithubApiService.ts
@@ -120,18 +120,18 @@ export class GithubApiService extends GithubRepository {
CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY,
);
const response = await retry.fetch>(async ({ attempt }) => {
- return await soxa.post("", {}, {
+ const res = await soxa.post("", {}, {
data: { query: query, variables },
headers: {
Authorization: `bearer ${TOKENS[attempt]}`,
},
});
+ if (res?.data?.errors) {
+ return this.handleError(res?.data?.errors);
+ }
+ return res;
}) as QueryDefaultResponse<{ user: T }>;
- if (response?.data?.errors) {
- return this.handleError(response?.data?.errors);
- }
-
return response?.data?.data?.user ??
new ServiceError("not found", EServiceKindError.NOT_FOUND);
} catch (error) {
@@ -146,7 +146,7 @@ export class GithubApiService extends GithubRepository {
Logger.error(error);
}
- return new ServiceError("not found", EServiceKindError.NOT_FOUND);
+ return new ServiceError("Rate limit exceeded", EServiceKindError.RATE_LIMIT);
}
}
}
From 5246b6791264bc7ac686b01af84b1ab16d6df96e Mon Sep 17 00:00:00 2001
From: Alex Oliveira
Date: Tue, 3 Oct 2023 00:04:16 +0100
Subject: [PATCH 18/33] fix: format
---
src/Services/GithubApiService.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/Services/GithubApiService.ts b/src/Services/GithubApiService.ts
index 8cdfd6c6..18c046e0 100644
--- a/src/Services/GithubApiService.ts
+++ b/src/Services/GithubApiService.ts
@@ -146,7 +146,10 @@ export class GithubApiService extends GithubRepository {
Logger.error(error);
}
- return new ServiceError("Rate limit exceeded", EServiceKindError.RATE_LIMIT);
+ return new ServiceError(
+ "Rate limit exceeded",
+ EServiceKindError.RATE_LIMIT,
+ );
}
}
}
From 22a9bc4112e77c266eb9b511abdaec15c28cc033 Mon Sep 17 00:00:00 2001
From: Alexandro Castro <10711649+AlexcastroDev@users.noreply.github.com>
Date: Tue, 3 Oct 2023 15:50:52 +0100
Subject: [PATCH 19/33] chore: retries improvements (#230)
* chore: retries improvements
* chore: improvements on retries
* chore: improvements on retries
* chore: add unit test
* chore: format
* chore: format
* chore: format
* fix: remove console
---
.gitignore | 3 +-
deps.ts | 12 +-
src/.DS_Store | Bin 6148 -> 0 bytes
src/Helpers/Retry.ts | 20 +-
src/Services/GithubApiService.ts | 60 +-
src/Services/__mocks__/notFoundUserMock.json | 20 +
src/Services/__mocks__/rateLimitMock.json | 18 +
.../__mocks__/successGithubResponse.json | 1541 +++++++++++++++++
.../__tests__/githubApiService.test.ts | 145 ++
src/Services/request.ts | 62 +
src/Types/Request.ts | 12 +-
src/Types/ServiceError.ts | 1 +
12 files changed, 1843 insertions(+), 51 deletions(-)
delete mode 100644 src/.DS_Store
create mode 100644 src/Services/__mocks__/notFoundUserMock.json
create mode 100644 src/Services/__mocks__/rateLimitMock.json
create mode 100644 src/Services/__mocks__/successGithubResponse.json
create mode 100644 src/Services/__tests__/githubApiService.test.ts
create mode 100644 src/Services/request.ts
diff --git a/.gitignore b/.gitignore
index 91b922d4..d59edcf0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.vscode
.env
.idea
-.lock
+deno.lock
*.sh
+**/.DS_Store
diff --git a/deps.ts b/deps.ts
index 4b51bfce..691ef991 100644
--- a/deps.ts
+++ b/deps.ts
@@ -6,7 +6,9 @@ import {
} from "https://deno.land/std@0.203.0/assert/mod.ts";
import {
assertSpyCalls,
+ returnsNext,
spy,
+ stub,
} from "https://deno.land/std@0.203.0/testing/mock.ts";
import { CONSTANTS } from "./src/utils.ts";
@@ -18,4 +20,12 @@ const soxa = new ServiceProvider({
baseURL,
});
-export { assertEquals, assertRejects, assertSpyCalls, soxa, spy };
+export {
+ assertEquals,
+ assertRejects,
+ assertSpyCalls,
+ returnsNext,
+ soxa,
+ spy,
+ stub,
+};
diff --git a/src/.DS_Store b/src/.DS_Store
deleted file mode 100644
index dbcdcabe06f6c0b9f79fd341be39ab7c161065a8..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6148
zcmeH~%}T>i5QWcZ7X>$6c6ndHHwdM^fO!GMmLfYKm=w9c<-ex?^3mjfCz}d4*~x^G`ee7
zI;O^_gG-D6)CI#~ypCCd+B`t*O2<@2XqKhaEVUXjEXx^hmDiPysaX!I;lt|5RuhWF
z(|LZ2bXb?FRRl!fn!s&tSKj|0=)cVW*G1ZifC&660=C%p+YMi-dh6unyw^7R6aCwm
o8|hr46;q=XbK|XeeUaDvn)kcXF*V8=k8-Mh1e}XZ1pb1+7jJhOK>z>%
diff --git a/src/Helpers/Retry.ts b/src/Helpers/Retry.ts
index a7ce9a95..7718f78a 100644
--- a/src/Helpers/Retry.ts
+++ b/src/Helpers/Retry.ts
@@ -1,3 +1,4 @@
+import { ServiceError } from "../Types/index.ts";
import { Logger } from "./Logger.ts";
export type RetryCallbackProps = {
@@ -12,11 +13,17 @@ async function* createAsyncIterable(
delay: number,
) {
for (let i = 0; i < retries; i++) {
+ const isLastAttempt = i === retries - 1;
try {
const data = await callback({ attempt: i });
yield data;
return;
} catch (e) {
+ if (e instanceof ServiceError && isLastAttempt) {
+ yield e;
+ return;
+ }
+
yield null;
Logger.error(e);
await new Promise((resolve) => setTimeout(resolve, delay));
@@ -29,6 +36,7 @@ export class Retry {
async fetch(
callback: callbackType,
) {
+ let lastError = null;
for await (
const callbackResult of createAsyncIterable(
callback,
@@ -36,11 +44,19 @@ export class Retry {
this.retryDelay,
)
) {
- if (callbackResult) {
+ const isError = callbackResult instanceof Error;
+
+ if (callbackResult && !isError) {
return callbackResult as T;
}
+
+ if (isError) {
+ lastError = callbackResult;
+ }
}
- throw new Error(`Max retries (${this.maxRetries}) exceeded.`);
+ throw new Error(`Max retries (${this.maxRetries}) exceeded.`, {
+ cause: lastError,
+ });
}
}
diff --git a/src/Services/GithubApiService.ts b/src/Services/GithubApiService.ts
index 18c046e0..6960de68 100644
--- a/src/Services/GithubApiService.ts
+++ b/src/Services/GithubApiService.ts
@@ -12,13 +12,12 @@ import {
queryUserPullRequest,
queryUserRepository,
} from "../Schemas/index.ts";
-import { soxa } from "../../deps.ts";
import { Retry } from "../Helpers/Retry.ts";
-import { GithubError, QueryDefaultResponse } from "../Types/index.ts";
import { CONSTANTS } from "../utils.ts";
import { EServiceKindError } from "../Types/EServiceKindError.ts";
import { ServiceError } from "../Types/ServiceError.ts";
import { Logger } from "../Helpers/Logger.ts";
+import { requestGithubData } from "./request.ts";
// Need to be here - Exporting from another file makes array of null
export const TOKENS = [
@@ -59,6 +58,7 @@ export class GithubApiService extends GithubRepository {
async requestUserInfo(username: string): Promise {
// Avoid to call others if one of them is null
const repository = await this.requestUserRepository(username);
+
if (repository instanceof ServiceError) {
Logger.error(repository);
return repository;
@@ -78,7 +78,7 @@ export class GithubApiService extends GithubRepository {
if (status.includes("rejected")) {
Logger.error(`Can not find a user with username:' ${username}'`);
- return new ServiceError("not found", EServiceKindError.NOT_FOUND);
+ return new ServiceError("Not found", EServiceKindError.NOT_FOUND);
}
return new UserInfo(
@@ -89,27 +89,6 @@ export class GithubApiService extends GithubRepository {
);
}
- private handleError(responseErrors: GithubError[]): ServiceError {
- const errors = responseErrors ?? [];
-
- const isRateLimitExceeded = errors.some((error) =>
- error.type.includes(EServiceKindError.RATE_LIMIT) ||
- error.message.includes("rate limit")
- );
-
- if (isRateLimitExceeded) {
- throw new ServiceError(
- "Rate limit exceeded",
- EServiceKindError.RATE_LIMIT,
- );
- }
-
- throw new ServiceError(
- "unknown error",
- EServiceKindError.NOT_FOUND,
- );
- }
-
async executeQuery(
query: string,
variables: { [key: string]: string },
@@ -120,36 +99,25 @@ export class GithubApiService extends GithubRepository {
CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY,
);
const response = await retry.fetch>(async ({ attempt }) => {
- const res = await soxa.post("", {}, {
- data: { query: query, variables },
- headers: {
- Authorization: `bearer ${TOKENS[attempt]}`,
- },
- });
- if (res?.data?.errors) {
- return this.handleError(res?.data?.errors);
- }
- return res;
- }) as QueryDefaultResponse<{ user: T }>;
+ return await requestGithubData(
+ query,
+ variables,
+ TOKENS[attempt],
+ );
+ });
- return response?.data?.data?.user ??
- new ServiceError("not found", EServiceKindError.NOT_FOUND);
+ return response;
} catch (error) {
- if (error instanceof ServiceError) {
- Logger.error(error);
- return error;
+ if (error.cause instanceof ServiceError) {
+ Logger.error(error.cause.message);
+ return error.cause;
}
- // TODO: Move this to a logger instance later
if (error instanceof Error && error.cause) {
Logger.error(JSON.stringify(error.cause, null, 2));
} else {
Logger.error(error);
}
-
- return new ServiceError(
- "Rate limit exceeded",
- EServiceKindError.RATE_LIMIT,
- );
+ return new ServiceError("not found", EServiceKindError.NOT_FOUND);
}
}
}
diff --git a/src/Services/__mocks__/notFoundUserMock.json b/src/Services/__mocks__/notFoundUserMock.json
new file mode 100644
index 00000000..75a76f47
--- /dev/null
+++ b/src/Services/__mocks__/notFoundUserMock.json
@@ -0,0 +1,20 @@
+{
+ "data": {
+ "user": null
+ },
+ "errors": [
+ {
+ "type": "NOT_FOUND",
+ "path": [
+ "user"
+ ],
+ "locations": [
+ {
+ "line": 2,
+ "column": 5
+ }
+ ],
+ "message": "Could not resolve to a User with the login of 'alekinho'."
+ }
+ ]
+}
diff --git a/src/Services/__mocks__/rateLimitMock.json b/src/Services/__mocks__/rateLimitMock.json
new file mode 100644
index 00000000..c49ffb2a
--- /dev/null
+++ b/src/Services/__mocks__/rateLimitMock.json
@@ -0,0 +1,18 @@
+{
+ "exceeded": {
+ "data": {
+ "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#secondary-rate-limits",
+ "message": "You have exceeded a secondary rate limit. Please wait a few minutes before you try again. If you reach out to GitHub Support for help, please include the request ID DBD8:FB98:31801A8:3222432:65195FDB."
+ }
+ },
+ "rate_limit": {
+ "data": {
+ "errors": [
+ {
+ "type": "RATE_LIMITED",
+ "message": "API rate limit exceeded for user ID 10711649."
+ }
+ ]
+ }
+ }
+}
diff --git a/src/Services/__mocks__/successGithubResponse.json b/src/Services/__mocks__/successGithubResponse.json
new file mode 100644
index 00000000..d8198b52
--- /dev/null
+++ b/src/Services/__mocks__/successGithubResponse.json
@@ -0,0 +1,1541 @@
+{
+ "data": {
+ "data": {
+ "user": {
+ "repositories": {
+ "totalCount": 128,
+ "nodes": [
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 23
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 11
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 9
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 6
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 6
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Java"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 5
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 5
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Jupyter Notebook"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 5
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 4
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 3
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 2
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 2
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 2
+ }
+ },
+ {
+ "languages": {
+ "nodes": []
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "PHP"
+ },
+ {
+ "name": "Go"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Dockerfile"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Dockerfile"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Dart"
+ },
+ {
+ "name": "Swift"
+ },
+ {
+ "name": "Kotlin"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Vue"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Jupyter Notebook"
+ },
+ {
+ "name": "Python"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Dart"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "Swift"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Vue"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "PHP"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 1
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": []
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": []
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Shell"
+ },
+ {
+ "name": "Dockerfile"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Dockerfile"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": []
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "C#"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "PHP"
+ },
+ {
+ "name": "Vue"
+ },
+ {
+ "name": "Blade"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "C"
+ },
+ {
+ "name": "C++"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "C++"
+ },
+ {
+ "name": "Makefile"
+ },
+ {
+ "name": "CMake"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "C++"
+ },
+ {
+ "name": "Makefile"
+ },
+ {
+ "name": "CMake"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Vue"
+ },
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": []
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "PHP"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Blade"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Rust"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Svelte"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "Dockerfile"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Rust"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Dockerfile"
+ },
+ {
+ "name": "Shell"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "Shell"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Dart"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "Swift"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Dart"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Rust"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "Swift"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "SCSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "Shell"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "SCSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "CSS"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "SCSS"
+ },
+ {
+ "name": "JavaScript"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "SCSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "JavaScript"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "SCSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "Swift"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ },
+ {
+ "languages": {
+ "nodes": [
+ {
+ "name": "TypeScript"
+ },
+ {
+ "name": "HTML"
+ },
+ {
+ "name": "CSS"
+ }
+ ]
+ },
+ "stargazers": {
+ "totalCount": 0
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/src/Services/__tests__/githubApiService.test.ts b/src/Services/__tests__/githubApiService.test.ts
new file mode 100644
index 00000000..99c5935b
--- /dev/null
+++ b/src/Services/__tests__/githubApiService.test.ts
@@ -0,0 +1,145 @@
+import { GithubApiService } from "../GithubApiService.ts";
+import { assertEquals, returnsNext, soxa, stub } from "../../../deps.ts";
+import { GitHubUserRepository } from "../../user_info.ts";
+
+const rateLimitMock = await import("../__mocks__/rateLimitMock.json", {
+ assert: { type: "json" },
+});
+
+const successGithubResponseMock = await import(
+ "../__mocks__/successGithubResponse.json",
+ { assert: { type: "json" } }
+);
+
+const notFoundGithubResponseMock = await import(
+ "../__mocks__/notFoundUserMock.json",
+ { assert: { type: "json" } }
+);
+
+import { ServiceError } from "../../Types/index.ts";
+
+// Unfortunatelly, The spy is a global instance
+// We can't reset mock as Jest does.
+stub(
+ soxa,
+ "post",
+ returnsNext([
+ // Should get data in first try
+ new Promise((resolve) => {
+ resolve(successGithubResponseMock.default);
+ }),
+ // // Should get data in second Retry
+ new Promise((resolve) => {
+ resolve(rateLimitMock.default.rate_limit);
+ }),
+ new Promise((resolve) => {
+ resolve(successGithubResponseMock.default);
+ }),
+ // Should throw NOT FOUND
+ new Promise((resolve) => {
+ resolve(notFoundGithubResponseMock.default);
+ }),
+ new Promise((resolve) => {
+ resolve(notFoundGithubResponseMock.default);
+ }),
+ // Should throw NOT FOUND even if request the user only
+ new Promise((resolve) => {
+ resolve(notFoundGithubResponseMock.default);
+ }),
+ new Promise((resolve) => {
+ resolve(notFoundGithubResponseMock.default);
+ }),
+ // Should throw RATE LIMIT
+ new Promise((resolve) => {
+ resolve(rateLimitMock.default.rate_limit);
+ }),
+ new Promise((resolve) => {
+ resolve(rateLimitMock.default.rate_limit);
+ }),
+ // Should throw RATE LIMIT Exceed
+ new Promise((resolve) => {
+ resolve(rateLimitMock.default.rate_limit);
+ }),
+ new Promise((resolve) => {
+ resolve(rateLimitMock.default.exceeded);
+ }),
+ ]),
+);
+
+Deno.test("Should get data in first try", async () => {
+ const provider = new GithubApiService();
+
+ const data = await provider.requestUserRepository(
+ "test",
+ ) as GitHubUserRepository;
+
+ assertEquals(data.repositories.totalCount, 128);
+});
+
+Deno.test("Should get data in second Retry", async () => {
+ const provider = new GithubApiService();
+
+ const data = await provider.requestUserRepository(
+ "test",
+ ) as GitHubUserRepository;
+
+ assertEquals(data.repositories.totalCount, 128);
+});
+
+Deno.test("Should throw NOT FOUND", async () => {
+ const provider = new GithubApiService();
+ let error = null;
+
+ try {
+ error = await provider.requestUserInfo("test");
+ } catch (e) {
+ error = e;
+ }
+
+ assertEquals(error.code, 404);
+ assertEquals(error instanceof ServiceError, true);
+});
+Deno.test("Should throw NOT FOUND even if request the user only", async () => {
+ const provider = new GithubApiService();
+ let error = null;
+
+ try {
+ error = await provider.requestUserRepository("test");
+ } catch (e) {
+ error = e;
+ }
+
+ assertEquals(error.code, 404);
+ assertEquals(error instanceof ServiceError, true);
+});
+
+// The assertRejects() assertion is a little more complicated
+// mainly because it deals with Promises.
+// https://docs.deno.com/runtime/manual/basics/testing/assertions#throws
+Deno.test("Should throw RATE LIMIT", async () => {
+ const provider = new GithubApiService();
+ let error = null;
+
+ try {
+ error = await provider.requestUserRepository("test");
+ } catch (e) {
+ error = e;
+ }
+
+ assertEquals(error.code, 419);
+ assertEquals(error instanceof ServiceError, true);
+});
+
+Deno.test("Should throw RATE LIMIT Exceed", async () => {
+ const provider = new GithubApiService();
+ let error = null;
+
+ try {
+ error = await provider.requestUserRepository("test");
+ } catch (e) {
+ error = e;
+ }
+
+ assertEquals(error.code, 419);
+ assertEquals(error instanceof ServiceError, true);
+});
diff --git a/src/Services/request.ts b/src/Services/request.ts
new file mode 100644
index 00000000..8aff36f4
--- /dev/null
+++ b/src/Services/request.ts
@@ -0,0 +1,62 @@
+import { soxa } from "../../deps.ts";
+import {
+ EServiceKindError,
+ GithubErrorResponse,
+ GithubExceedError,
+ QueryDefaultResponse,
+ ServiceError,
+} from "../Types/index.ts";
+
+export async function requestGithubData(
+ query: string,
+ variables: { [key: string]: string },
+ token = "",
+) {
+ const response = await soxa.post("", {}, {
+ data: { query, variables },
+ headers: {
+ Authorization: `bearer ${token}`,
+ },
+ }) as QueryDefaultResponse<{ user: T }>;
+ const responseData = response.data;
+
+ if (responseData?.data?.user) {
+ return responseData.data.user;
+ }
+
+ throw handleError(
+ responseData as unknown as GithubErrorResponse | GithubExceedError,
+ );
+}
+
+function handleError(
+ reponseErrors: GithubErrorResponse | GithubExceedError,
+): ServiceError {
+ let isRateLimitExceeded = false;
+ const arrayErrors = (reponseErrors as GithubErrorResponse)?.errors || [];
+ const objectError = (reponseErrors as GithubExceedError) || {};
+
+ if (Array.isArray(arrayErrors)) {
+ isRateLimitExceeded = arrayErrors.some((error) =>
+ error.type.includes(EServiceKindError.RATE_LIMIT)
+ );
+ }
+
+ if (objectError?.message) {
+ isRateLimitExceeded = objectError?.message.includes(
+ "rate limit",
+ );
+ }
+
+ if (isRateLimitExceeded) {
+ throw new ServiceError(
+ "Rate limit exceeded",
+ EServiceKindError.RATE_LIMIT,
+ );
+ }
+
+ throw new ServiceError(
+ "unknown error",
+ EServiceKindError.NOT_FOUND,
+ );
+}
diff --git a/src/Types/Request.ts b/src/Types/Request.ts
index 85f1362c..e0c97ada 100644
--- a/src/Types/Request.ts
+++ b/src/Types/Request.ts
@@ -3,9 +3,19 @@ export type GithubError = {
type: string;
};
+export type GithubErrorResponse = {
+ errors: GithubError[];
+};
+
+export type GithubExceedError = {
+ documentation_url: string;
+ message: string;
+};
+
export type QueryDefaultResponse = {
data: {
data: T;
- errors: GithubError[];
+ errors?: GithubErrorResponse;
+ message?: string;
};
};
diff --git a/src/Types/ServiceError.ts b/src/Types/ServiceError.ts
index 8a0db072..cd0ec7a2 100644
--- a/src/Types/ServiceError.ts
+++ b/src/Types/ServiceError.ts
@@ -3,6 +3,7 @@ import { EServiceKindError } from "./EServiceKindError.ts";
export class ServiceError extends Error {
constructor(message: string, kind: EServiceKindError) {
super(message);
+ this.message = message;
this.name = "ServiceError";
this.cause = kind;
}
From 0cc5b5cbc72d3b924bcf2629280183dc0dc5dc4d Mon Sep 17 00:00:00 2001
From: Bhav Beri <43399374+bhavberi@users.noreply.github.com>
Date: Tue, 3 Oct 2023 20:28:45 +0530
Subject: [PATCH 20/33] Added Account Duration/Experience Trophy (#203)
* Added Account Duration/Experience Trophy
* Minor Formatting Changes
* deno fmt correction
---------
Co-authored-by: Alexandro Castro <10711649+AlexcastroDev@users.noreply.github.com>
---
src/trophy.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++++
src/trophy_list.ts | 2 ++
src/user_info.ts | 5 +++++
3 files changed, 58 insertions(+)
diff --git a/src/trophy.ts b/src/trophy.ts
index 778f30d1..24dc708e 100644
--- a/src/trophy.ts
+++ b/src/trophy.ts
@@ -267,6 +267,57 @@ export class TotalReviewsTrophy extends Trophy {
}
}
+export class AccountDurationTrophy extends Trophy {
+ constructor(score: number) {
+ const rankConditions = [
+ new RankCondition(
+ RANK.SSS,
+ "Seasoned Veteran",
+ 70, // 20 years
+ ),
+ new RankCondition(
+ RANK.SS,
+ "GranMaster",
+ 55, // 15 years
+ ),
+ new RankCondition(
+ RANK.S,
+ "Master Dev",
+ 40, // 10 years
+ ),
+ new RankCondition(
+ RANK.AAA,
+ "Expert Dev",
+ 28, // 7.5 years
+ ),
+ new RankCondition(
+ RANK.AA,
+ "Experienced Dev",
+ 18, // 5 years
+ ),
+ new RankCondition(
+ RANK.A,
+ "Intermediate Dev",
+ 11, // 3 years
+ ),
+ new RankCondition(
+ RANK.B,
+ "Junior Dev",
+ 6, // 1.5 years
+ ),
+ new RankCondition(
+ RANK.C,
+ "Newbie",
+ 2, // 0.5 year
+ ),
+ ];
+ super(score, rankConditions);
+ this.title = "Experience";
+ this.filterTitles = ["Experience", "Duration", "Since"];
+ // this.hidden = true;
+ }
+}
+
export class TotalStarTrophy extends Trophy {
constructor(score: number) {
const rankConditions = [
diff --git a/src/trophy_list.ts b/src/trophy_list.ts
index d8e406f3..2fcab4c8 100644
--- a/src/trophy_list.ts
+++ b/src/trophy_list.ts
@@ -1,4 +1,5 @@
import {
+ AccountDurationTrophy,
AllSuperRankTrophy,
AncientAccountTrophy,
Joined2020Trophy,
@@ -40,6 +41,7 @@ export class TrophyList {
new OGAccountTrophy(userInfo.ogAccount),
new Joined2020Trophy(userInfo.joined2020),
new MultipleOrganizationsTrophy(userInfo.totalOrganizations),
+ new AccountDurationTrophy(userInfo.durationDays),
);
}
get length() {
diff --git a/src/user_info.ts b/src/user_info.ts
index 7bfdac88..805402d6 100644
--- a/src/user_info.ts
+++ b/src/user_info.ts
@@ -51,6 +51,7 @@ export class UserInfo {
public readonly totalRepositories: number;
public readonly languageCount: number;
public readonly durationYear: number;
+ public readonly durationDays: number;
public readonly ancientAccount: number;
public readonly joined2020: number;
public readonly ogAccount: number;
@@ -83,6 +84,9 @@ export class UserInfo {
const durationTime = new Date().getTime() -
new Date(userActivity.createdAt).getTime();
const durationYear = new Date(durationTime).getUTCFullYear() - 1970;
+ const durationDays = Math.floor(
+ durationTime / (1000 * 60 * 60 * 24) / 100,
+ );
const ancientAccount =
new Date(userActivity.createdAt).getFullYear() <= 2010 ? 1 : 0;
const joined2020 = new Date(userActivity.createdAt).getFullYear() == 2020
@@ -104,6 +108,7 @@ export class UserInfo {
this.totalRepositories = userRepository.repositories.totalCount;
this.languageCount = languages.size;
this.durationYear = durationYear;
+ this.durationDays = durationDays;
this.ancientAccount = ancientAccount;
this.joined2020 = joined2020;
this.ogAccount = ogAccount;
From 24d831db67b14254604ce16372f8f1af519eda4b Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Wed, 4 Oct 2023 00:01:46 +0900
Subject: [PATCH 21/33] Fix deno version
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e8cf04a8..f5576ac9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@
## Environment
-- Deno >= v1.9.2
+- Deno >= v1.36.1
- [Vercel](https://vercel.com/)
- GitHub API v4
From 05f2bb92c1a0fc556e1fc34c3037ddc9718fcf62 Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Wed, 4 Oct 2023 00:03:26 +0900
Subject: [PATCH 22/33] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 96bc8cfd..55dae963 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ readme, change the `?username=` value to your GitHub's username.
```
-
+
## Use theme
From dc043fbe7878d0cb71a167c39d4e0d2cf87d8b15 Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Wed, 4 Oct 2023 00:06:18 +0900
Subject: [PATCH 23/33] Change DEFAULT_MAX_COLUMN to 8
---
src/utils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/utils.ts b/src/utils.ts
index 0ed5cd30..a3e21fa7 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -58,7 +58,7 @@ const HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
export const CONSTANTS = {
CACHE_MAX_AGE: 7200,
DEFAULT_PANEL_SIZE: 110,
- DEFAULT_MAX_COLUMN: 6,
+ DEFAULT_MAX_COLUMN: 8,
DEFAULT_MAX_ROW: 3,
DEFAULT_MARGIN_W: 0,
DEFAULT_MARGIN_H: 0,
From 614e0453d0cc7e4d5c2455985cb470c66b827cb6 Mon Sep 17 00:00:00 2001
From: Alexandro Castro <10711649+AlexcastroDev@users.noreply.github.com>
Date: Tue, 3 Oct 2023 16:38:18 +0100
Subject: [PATCH 24/33] feat: add redis cache (#231)
* feat: add redis cache
* chore: add TTL
* chore: change port
---
CONTRIBUTING.md | 9 ++++++
api/index.ts | 29 ++++++++++++------
deps.ts | 6 ++++
docker-compose.yml | 7 +++++
env-example | 8 +++++
src/config/cache.ts | 74 +++++++++++++++++++++++++++++++++++++++++++++
src/utils.ts | 1 +
7 files changed, 124 insertions(+), 10 deletions(-)
create mode 100644 docker-compose.yml
create mode 100644 env-example
create mode 100644 src/config/cache.ts
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f5576ac9..7306a082 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,6 +5,7 @@
- Deno >= v1.36.1
- [Vercel](https://vercel.com/)
- GitHub API v4
+- Docker and Docker compose (optional)
## Local Run
@@ -26,6 +27,14 @@ Run local server.
deno task start
```
+You can enable the Redis if you want, but it's not mandatory.
+
+```sh
+docker compose up -d
+```
+
+Rename `env-example` to `.env`, and change ENABLE_REDIS to true
+
Open localhost from your browser.
http://localhost:8080/?username=ryo-ma
diff --git a/api/index.ts b/api/index.ts
index 68d5215a..059fdeb0 100644
--- a/api/index.ts
+++ b/api/index.ts
@@ -8,6 +8,7 @@ import { GithubRepositoryService } from "../src/Repository/GithubRepository.ts";
import { GithubApiService } from "../src/Services/GithubApiService.ts";
import { ServiceError } from "../src/Types/index.ts";
import { ErrorPage } from "../src/pages/Error.ts";
+import { cacheProvider } from "../src/config/cache.ts";
const serviceProvider = new GithubApiService();
const client = new GithubRepositoryService(serviceProvider).repository;
@@ -75,17 +76,25 @@ async function app(req: Request): Promise {
},
);
}
- const userInfo = await client.requestUserInfo(username);
- if (userInfo instanceof ServiceError) {
- return new Response(
- ErrorPage({ error: userInfo, username }).render(),
- {
- status: userInfo.code,
- headers: new Headers({ "Content-Type": "text" }),
- },
- );
- }
+ const userKeyCache = ["v1", username].join("-");
+ const userInfoCached = await cacheProvider.get(userKeyCache) || "{}";
+ let userInfo = JSON.parse(userInfoCached);
+ const hasCache = !!Object.keys(userInfo).length;
+ if (!hasCache) {
+ const userResponseInfo = await client.requestUserInfo(username);
+ if (userResponseInfo instanceof ServiceError) {
+ return new Response(
+ ErrorPage({ error: userInfo, username }).render(),
+ {
+ status: userResponseInfo.code,
+ headers: new Headers({ "Content-Type": "text" }),
+ },
+ );
+ }
+ userInfo = userResponseInfo;
+ await cacheProvider.set(userKeyCache, JSON.stringify(userInfo));
+ }
// Success Response
return new Response(
new Card(
diff --git a/deps.ts b/deps.ts
index 691ef991..646eaf28 100644
--- a/deps.ts
+++ b/deps.ts
@@ -11,6 +11,12 @@ import {
stub,
} from "https://deno.land/std@0.203.0/testing/mock.ts";
+export {
+ type Bulk,
+ connect,
+ type Redis,
+} from "https://deno.land/x/redis@v0.31.0/mod.ts";
+
import { CONSTANTS } from "./src/utils.ts";
const baseURL = Deno.env.get("GITHUB_API") || CONSTANTS.DEFAULT_GITHUB_API;
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..9036241a
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,7 @@
+version: "3"
+services:
+ redis:
+ container_name: trophy-redis
+ image: redis:latest
+ ports:
+ - "6379:6379"
diff --git a/env-example b/env-example
new file mode 100644
index 00000000..6f5a02bb
--- /dev/null
+++ b/env-example
@@ -0,0 +1,8 @@
+GITHUB_TOKEN1=
+GITHUB_TOKEN2=
+GITHUB_API=https://api.github.com/graphql
+ENABLE_REDIS=
+REDIS_PORT=6379
+REDIS_HOST=
+REDIS_USERNAME=
+REDIS_PASSWORD=
diff --git a/src/config/cache.ts b/src/config/cache.ts
new file mode 100644
index 00000000..4f6aef3f
--- /dev/null
+++ b/src/config/cache.ts
@@ -0,0 +1,74 @@
+import { Bulk, connect, Redis } from "../../deps.ts";
+import { Logger } from "../Helpers/Logger.ts";
+import { CONSTANTS } from "../utils.ts";
+
+const enableCache = Deno.env.get("ENABLE_REDIS") || false;
+
+// https://developer.redis.com/develop/deno/
+class CacheProvider {
+ private static instance: CacheProvider;
+ public client: Redis | null = null;
+
+ private constructor() {}
+
+ static getInstance(): CacheProvider {
+ if (!CacheProvider.instance) {
+ CacheProvider.instance = new CacheProvider();
+ }
+ return CacheProvider.instance;
+ }
+
+ async connect(): Promise {
+ if (!enableCache) return;
+ this.client = await connect({
+ hostname: Deno.env.get("REDIS_HOST") || "",
+ port: Number(Deno.env.get("REDIS_PORT")) || 6379,
+ username: Deno.env.get("REDIS_USERNAME") || "",
+ password: Deno.env.get("REDIS_PASSWORD") || "",
+ });
+ }
+
+ async get(key: string): Promise {
+ if (!enableCache) return undefined;
+
+ try {
+ if (!this.client) {
+ await this.connect();
+ }
+
+ return await this.client?.get(key);
+ } catch {
+ return undefined;
+ }
+ }
+
+ async set(key: string, value: string): Promise {
+ if (!enableCache) return;
+
+ try {
+ if (!this.client) {
+ await this.connect();
+ }
+ await this.client?.set(key, value, {
+ px: CONSTANTS.REDIS_TTL,
+ });
+ } catch (e) {
+ Logger.error(`Failed to set cache: ${e.message}`);
+ }
+ }
+
+ async del(key: string): Promise {
+ if (!enableCache) return;
+
+ try {
+ if (!this.client) {
+ await this.connect();
+ }
+ await this.client?.del(key);
+ } catch (e) {
+ Logger.error(`Failed to delete cache: ${e.message}`);
+ }
+ }
+}
+
+export const cacheProvider = CacheProvider.getInstance();
diff --git a/src/utils.ts b/src/utils.ts
index a3e21fa7..a244d110 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -67,6 +67,7 @@ export const CONSTANTS = {
DEFAULT_GITHUB_API: "https://api.github.com/graphql",
DEFAULT_GITHUB_RETRY_DELAY: 1000,
REVALIDATE_TIME: HOUR_IN_MILLISECONDS,
+ REDIS_TTL: HOUR_IN_MILLISECONDS * 4,
};
export enum RANK {
From 8c8a5a91d092f242a41cabc3d24963164c900f9c Mon Sep 17 00:00:00 2001
From: SmashedFrenzy16
Date: Wed, 4 Oct 2023 07:41:08 +0100
Subject: [PATCH 25/33] Update trophy.ts with spelling correction (#232)
---
src/trophy.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/trophy.ts b/src/trophy.ts
index 24dc708e..14602c6c 100644
--- a/src/trophy.ts
+++ b/src/trophy.ts
@@ -277,7 +277,7 @@ export class AccountDurationTrophy extends Trophy {
),
new RankCondition(
RANK.SS,
- "GranMaster",
+ "Grandmaster",
55, // 15 years
),
new RankCondition(
From 501d8159b77d221b65da518f297a4fd5dcc126d9 Mon Sep 17 00:00:00 2001
From: SmashedFrenzy16
Date: Wed, 4 Oct 2023 14:58:56 +0100
Subject: [PATCH 26/33] Update trophy_list.ts with correct function name (#233)
---
src/trophy_list.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/trophy_list.ts b/src/trophy_list.ts
index 2fcab4c8..1f190d9b 100644
--- a/src/trophy_list.ts
+++ b/src/trophy_list.ts
@@ -55,7 +55,7 @@ export class TrophyList {
? 1
: 0;
}
- filterByHideen() {
+ filterByHidden() {
this.trophies = this.trophies.filter((trophy) =>
!trophy.hidden || trophy.rank !== RANK.UNKNOWN
);
From cb822ea356818df226cc3bb3229cc7158584ec72 Mon Sep 17 00:00:00 2001
From: Alex Oliveira
Date: Wed, 4 Oct 2023 17:18:12 +0100
Subject: [PATCH 27/33] hotfix: function name
---
src/card.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/card.ts b/src/card.ts
index 3403c97e..70e39f49 100644
--- a/src/card.ts
+++ b/src/card.ts
@@ -26,7 +26,7 @@ export class Card {
): string {
const trophyList = new TrophyList(userInfo);
- trophyList.filterByHideen();
+ trophyList.filterByHidden();
if (this.titles.length != 0) {
trophyList.filterByTitles(this.titles);
From 8761a4ca03313b1bc657efa2e49e802b97d319c5 Mon Sep 17 00:00:00 2001
From: CodeMaster7000
Date: Wed, 4 Oct 2023 23:46:43 +0100
Subject: [PATCH 28/33] Add license info and link (#235)
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 55dae963..941dff2b 100644
--- a/README.md
+++ b/README.md
@@ -504,6 +504,10 @@ https://github-profile-trophy.vercel.app/?username=ryo-ma&no-frame=true
Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
+# License
+
+This product is licensed under the [MIT License](https://github.com/ryo-ma/github-profile-trophy/blob/master/LICENSE).
+
# Testing
```bash
From c74a43c12e4009df4cd76606e8ff3ce854a40d55 Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Sat, 7 Oct 2023 08:19:01 +0900
Subject: [PATCH 29/33] Update CONTRIBUTING.md
---
CONTRIBUTING.md | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7306a082..c0739ed6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -43,11 +43,21 @@ http://localhost:8080/?username=ryo-ma
Read the [.editorconfig](./.editorconfig)
-## Run deno lint
+## What to do before contributing
-If you want to contribute to my project, you should check the lint with the
-following command.
+### 1. Run deno lint
```sh
-deno lint --unstable
+deno task lint
+```
+
+### 2. Run deno fmt
+
+```sh
+deno task fmt
+```
+### 3. Run deno test
+
+```sh
+deno task test
```
From 871dc99651e0dd1062f26e8d8689334a4bd288a9 Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Sat, 7 Oct 2023 08:20:02 +0900
Subject: [PATCH 30/33] Update README.md
---
README.md | 6 ------
1 file changed, 6 deletions(-)
diff --git a/README.md b/README.md
index 941dff2b..b21d0154 100644
--- a/README.md
+++ b/README.md
@@ -507,9 +507,3 @@ Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
# License
This product is licensed under the [MIT License](https://github.com/ryo-ma/github-profile-trophy/blob/master/LICENSE).
-
-# Testing
-
-```bash
-deno task test
-```
From 866cdcc5cdb1493d381d8a5ce349be2f2e84277a Mon Sep 17 00:00:00 2001
From: CodeMaster7000
Date: Sun, 8 Oct 2023 14:16:41 +0100
Subject: [PATCH 31/33] Update CONTRIBUTING.md to add a contribution guideline
(#238)
* Update CONTRIBUTING.md to add a contribution guideline
* Update CONTRIBUTING.md
---
CONTRIBUTING.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c0739ed6..15a6ad51 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -43,6 +43,15 @@ http://localhost:8080/?username=ryo-ma
Read the [.editorconfig](./.editorconfig)
+## Pull Requests
+
+Pull requests are are always welcome! In general, they should a single concern
+in the least number of changed lines as possible. For changes that address core
+functionality, it is best to open an issue to discuss your proposal first. I
+look forward to seeing what you come up with!
+
+## Run deno lint
+
## What to do before contributing
### 1. Run deno lint
From 52e1e38caa24bbe0b013522e616445b3d9b6e5bb Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Sun, 8 Oct 2023 22:17:38 +0900
Subject: [PATCH 32/33] Fix format
---
CONTRIBUTING.md | 1 +
README.md | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 15a6ad51..ab80ab54 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,6 +65,7 @@ deno task lint
```sh
deno task fmt
```
+
### 3. Run deno test
```sh
diff --git a/README.md b/README.md
index b21d0154..adc0c19b 100644
--- a/README.md
+++ b/README.md
@@ -506,4 +506,5 @@ Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
# License
-This product is licensed under the [MIT License](https://github.com/ryo-ma/github-profile-trophy/blob/master/LICENSE).
+This product is licensed under the
+[MIT License](https://github.com/ryo-ma/github-profile-trophy/blob/master/LICENSE).
From 0a3c385fc5b1e26ac55a67845d6daf49932c89f8 Mon Sep 17 00:00:00 2001
From: ryo-ma
Date: Sun, 8 Oct 2023 22:19:14 +0900
Subject: [PATCH 33/33] Update testing.yml
---
.github/workflows/testing.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 8d42acdc..a25b6fca 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -1,6 +1,9 @@
name: Check PR Test
on:
+ push:
+ branches:
+ - master
pull_request:
branches:
- master