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/api/index.ts b/api/index.ts index 68d5215a..9935a744 100644 --- a/api/index.ts +++ b/api/index.ts @@ -4,13 +4,9 @@ import { COLORS, Theme } from "../src/theme.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; +import { client } from "../src/Services/index.ts"; const defaultHeaders = new Headers( { diff --git a/deps.ts b/deps.ts index 4b51bfce..846cc99e 100644 --- a/deps.ts +++ b/deps.ts @@ -6,16 +6,31 @@ 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"; +const api = new Map([ + ["github", Deno.env.get("GITHUB_API")], + ["azure", Deno.env.get("AZURE_API_URL")], +]); -const baseURL = Deno.env.get("GITHUB_API") || CONSTANTS.DEFAULT_GITHUB_API; +const DEFAULT_PROVIDER = Deno.env.get("provider") ?? "github"; + +const baseURL = api.get(DEFAULT_PROVIDER); const soxa = new ServiceProvider({ ...defaults, 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 dbcdcabe..00000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/src/Helpers/Retry.ts b/src/Helpers/Retry.ts index a7ce9a95..a1d79eaf 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 = { @@ -17,6 +18,11 @@ async function* createAsyncIterable( yield data; return; } catch (e) { + if (e instanceof ServiceError && i < (retries - 1)) { + yield e; + return; + } + yield null; Logger.error(e); await new Promise((resolve) => setTimeout(resolve, delay)); diff --git a/src/Services/GithubApiService.ts b/src/Services/GithubApiService.ts index 8cdfd6c6..fbaff484 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; @@ -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,33 +99,26 @@ 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; } - // 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/GithubAzureService.ts b/src/Services/GithubAzureService.ts new file mode 100644 index 00000000..28900297 --- /dev/null +++ b/src/Services/GithubAzureService.ts @@ -0,0 +1,128 @@ +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 { + EServiceKindError, + QueryDefaultResponse, + ServiceError, +} from "../Types/index.ts"; +import { CONSTANTS } from "../utils.ts"; +import { Logger } from "../Helpers/Logger.ts"; + +const authentication = Deno.env.get("X_API_KEY"); + +export const TOKENS = [ + Deno.env.get("GITHUB_TOKEN1"), + Deno.env.get("GITHUB_TOKEN2"), +]; + +export class GithubAzureService extends GithubRepository { + async requestUserRepository( + username: string, + ): Promise { + return await this.executeQuery(queryUserRepository, { + username, + }, "userRepository"); + } + async requestUserActivity( + username: string, + ): Promise { + return await this.executeQuery(queryUserActivity, { + username, + }, "userActivity"); + } + async requestUserIssue( + username: string, + ): Promise { + return await this.executeQuery(queryUserIssue, { + username, + }, "userIssue"); + } + async requestUserPullRequest( + username: string, + ): Promise { + return await this.executeQuery( + queryUserPullRequest, + { username }, + "userPullRequest", + ); + } + 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 new ServiceError("not found", EServiceKindError.NOT_FOUND); + } + + 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")) { + 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 as GitHubUserRepository, + ); + } + + async executeQuery( + query: string, + variables: { [key: string]: string }, + cache_ns?: string, + ) { + const retry = new Retry( + TOKENS.length, + CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY, + ); + try { + const response = await retry.fetch>(async () => { + return await soxa.post("", {}, { + data: { query: query, variables }, + headers: { + cache_ns, + "x_api_key": authentication, + }, + }); + }) as QueryDefaultResponse<{ data: { user: T } }>; + + return response?.data?.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) { + Logger.error(JSON.stringify(error.cause, null, 2)); + } else { + Logger.error(error); + } + + return new ServiceError("not found", EServiceKindError.NOT_FOUND); + } + } +} diff --git a/src/Services/__mocks__/rateLimitMock.json b/src/Services/__mocks__/rateLimitMock.json new file mode 100644 index 00000000..1e943585 --- /dev/null +++ b/src/Services/__mocks__/rateLimitMock.json @@ -0,0 +1,14 @@ +{ + "exceeded": { + "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": { + "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..4940d309 --- /dev/null +++ b/src/Services/__mocks__/successGithubResponse.json @@ -0,0 +1,1535 @@ +{ + "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/index.ts b/src/Services/index.ts new file mode 100644 index 00000000..3d9d41bd --- /dev/null +++ b/src/Services/index.ts @@ -0,0 +1,23 @@ +import { GithubRepositoryService } from "../Repository/GithubRepository.ts"; +import { GithubApiService } from "./GithubApiService.ts"; +import { GithubAzureService } from "./GithubAzureService.ts"; + +export type Provider = GithubApiService | GithubAzureService; + +const DEFAULT_PROVIDER = Deno.env.get("provider") ?? "github"; + +const providers = new Map([ + ["github", GithubApiService], + ["azure", GithubAzureService], +]); + +const serviceProvider = providers.get(DEFAULT_PROVIDER); + +if (serviceProvider === undefined) { + throw new Error("Invalid provider"); +} + +const provider = new serviceProvider(); +const repositoryClient = new GithubRepositoryService(provider).repository; + +export const client = repositoryClient; diff --git a/src/Services/request.ts b/src/Services/request.ts new file mode 100644 index 00000000..9e356a77 --- /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; }; };