From ed1e514e772ae9b4a960e0b31fc88d9865d24221 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Fri, 29 Sep 2023 21:57:55 +0100 Subject: [PATCH] fix: type check error --- .github/workflows/testing.yml | 23 +-- .gitignore | 1 + CONTRIBUTING.md | 15 +- README.md | 159 +++++++++--------- api/index.ts | 13 +- deno.json | 9 + 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 | 46 ++--- src/StaticRenderRegeneration/types.ts | 10 +- src/StaticRenderRegeneration/utils.ts | 58 +++---- 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 +-- 27 files changed, 381 insertions(+), 245 deletions(-) create mode 100644 deno.json 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..6432a532 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -7,16 +7,19 @@ on: jobs: install-dependencies: runs-on: ubuntu-latest + + strategy: + matrix: + deno-version: [1.37.0] + 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: Lint Deno Module + run: deno fmt --check + - name: Test Deno Module + run: deno task test diff --git a/.gitignore b/.gitignore index 3c497e35..3cd929e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode .env .idea +deno.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..3b3d07a4 --- /dev/null +++ b/deno.json @@ -0,0 +1,9 @@ +{ + "tasks": { + "start": "deno -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/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..810eb85d 100644 --- a/src/StaticRenderRegeneration/index.ts +++ b/src/StaticRenderRegeneration/index.ts @@ -1,29 +1,33 @@ import { CacheManager } from "./cache_manager.ts"; import { StaticRegenerationOptions } from "./types.ts"; -import { getUrl, readCache, generateUUID } from "./utils.ts"; +import { generateUUID, getUrl, 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 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) + } + + const response = await render(request); + 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..77f1f907 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(''); + 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" + } + ] +}