Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: azure provider #227

Closed
wants to merge 12 commits into from
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.vscode
.env
.idea
.lock
deno.lock
*.sh
**/.DS_Store
6 changes: 1 addition & 5 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ import { COLORS, Theme } from "../src/theme.ts";
import { Error400 } from "../src/error_page.ts";
import "https://deno.land/x/[email protected]/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(
{
Expand Down
21 changes: 18 additions & 3 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,31 @@ import {
} from "https://deno.land/[email protected]/assert/mod.ts";
import {
assertSpyCalls,
returnsNext,
spy,
stub,
} from "https://deno.land/[email protected]/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,
};
Binary file removed src/.DS_Store
Binary file not shown.
6 changes: 6 additions & 0 deletions src/Helpers/Retry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ServiceError } from "../Types/index.ts";
import { Logger } from "./Logger.ts";

export type RetryCallbackProps = {
Expand All @@ -17,6 +18,11 @@ async function* createAsyncIterable<T>(
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));
Expand Down
48 changes: 10 additions & 38 deletions src/Services/GithubApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -59,6 +58,7 @@ export class GithubApiService extends GithubRepository {
async requestUserInfo(username: string): Promise<UserInfo | ServiceError> {
// 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;
Expand Down Expand Up @@ -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<T = unknown>(
query: string,
variables: { [key: string]: string },
Expand All @@ -120,33 +99,26 @@ export class GithubApiService extends GithubRepository {
CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY,
);
const response = await retry.fetch<Promise<T>>(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);
}
}
}
128 changes: 128 additions & 0 deletions src/Services/GithubAzureService.ts
Original file line number Diff line number Diff line change
@@ -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<GitHubUserRepository | ServiceError> {
return await this.executeQuery<GitHubUserRepository>(queryUserRepository, {
username,
}, "userRepository");
}
async requestUserActivity(
username: string,
): Promise<GitHubUserActivity | ServiceError> {
return await this.executeQuery<GitHubUserActivity>(queryUserActivity, {
username,
}, "userActivity");
}
async requestUserIssue(
username: string,
): Promise<GitHubUserIssue | ServiceError> {
return await this.executeQuery<GitHubUserIssue>(queryUserIssue, {
username,
}, "userIssue");
}
async requestUserPullRequest(
username: string,
): Promise<GitHubUserPullRequest | ServiceError> {
return await this.executeQuery<GitHubUserPullRequest>(
queryUserPullRequest,
{ username },
"userPullRequest",
);
}
async requestUserInfo(username: string): Promise<UserInfo | ServiceError> {
// 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<GitHubUserActivity>).value,
(issue as PromiseFulfilledResult<GitHubUserIssue>).value,
(pullRequest as PromiseFulfilledResult<GitHubUserPullRequest>).value,
repository as GitHubUserRepository,
);
}

async executeQuery<T = unknown>(
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<Promise<T>>(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);
}
}
}
14 changes: 14 additions & 0 deletions src/Services/__mocks__/rateLimitMock.json
Original file line number Diff line number Diff line change
@@ -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."
}
]
}
}
Loading
Loading