Skip to content

Commit

Permalink
Merge pull request #557 from dzcode-io/553-move-projects-endpoint-fro…
Browse files Browse the repository at this point in the history
…m-data-to-api
  • Loading branch information
ZibanPirate authored Mar 26, 2023
2 parents ff7c98d + c224cc9 commit 309d0c1
Show file tree
Hide file tree
Showing 20 changed files with 200 additions and 64 deletions.
2 changes: 2 additions & 0 deletions api/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { GithubController } from "src/github/controller";
import { GithubUserController } from "src/github-user/controller";
import { LoggerService } from "src/logger/service";
import { MilestoneController } from "src/milestone/controller";
import { ProjectController } from "src/project/controller";
import { TeamController } from "src/team/controller";
import Container from "typedi";

Expand Down Expand Up @@ -46,6 +47,7 @@ export const routingControllersOptions: RoutingControllersOptions = {
TeamController,
GithubController,
MilestoneController,
ProjectController,
],
middlewares: [
SentryRequestHandlerMiddleware,
Expand Down
28 changes: 11 additions & 17 deletions api/src/contribution/repository.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
import { getCollection } from "@dzcode.io/data/dist/get/collection";
import { Model } from "@dzcode.io/models/dist/_base";
import { ContributionEntity } from "@dzcode.io/models/dist/contribution";
import { ProjectReferenceEntity } from "@dzcode.io/models/dist/project-reference";
import { join } from "path";
import { DataService } from "src/data/service";
import { GithubService } from "src/github/service";
import { Service } from "typedi";

import { allFilterNames, FilterDto, GetContributionsResponseDto, OptionDto } from "./types";

@Service()
export class ContributionRepository {
constructor(private readonly githubService: GithubService) {
const projects = getCollection<Model<ProjectReferenceEntity, "repositories">>(
join(__dirname, "../../../data"),
"projects-v2",
"list.json",
);
this.projects = projects !== 404 ? projects : [];
}

private projects: Model<ProjectReferenceEntity, "repositories">[];
constructor(
private readonly githubService: GithubService,
private readonly dataService: DataService,
) {}

public async find(
filterFn?: (value: ContributionEntity, index: number, array: ContributionEntity[]) => boolean,
): Promise<Pick<GetContributionsResponseDto, "contributions" | "filters">> {
const projects = await this.dataService.listProjects();

let contributions = (
await Promise.all(
this.projects.reduce<Promise<Model<ContributionEntity, "project">[]>[]>(
projects.reduce<Promise<Model<ContributionEntity, "project">[]>[]>(
(pV, { repositories, name, slug }) => [
...pV,
...repositories
.filter(({ provider }) => provider === "github")
.map(async ({ owner, repository: repo }) => {
.map(async ({ owner, repository }) => {
const issuesIncludingPRs = await this.githubService.listRepositoryIssues({
owner,
repo,
repository,
});

const languages = await this.githubService.listRepositoryLanguages({
owner,
repo,
repository,
});
// @TODO-ZM: filter out the ones created by bots
return issuesIncludingPRs.map<Model<ContributionEntity, "project">>(
Expand Down
4 changes: 2 additions & 2 deletions api/src/contributor/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export class ContributorController {
// current place for data:
this.githubService.listContributors({
owner: "dzcode-io",
repo: "dzcode.io",
repository: "dzcode.io",
path: `data/models/${path}`,
}),
// also check old place for data, to not lose contribution effort:
this.githubService.listContributors({
owner: "dzcode-io",
repo: "dzcode.io",
repository: "dzcode.io",
path: `data/${path}`,
}),
]);
Expand Down
25 changes: 25 additions & 0 deletions api/src/data/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getCollection } from "@dzcode.io/data/dist/get/collection";
import { join } from "path";
import { ConfigService } from "src/config/service";
import { Service } from "typedi";

import { ProjectReferenceEntity } from "./types";

@Service()
export class DataService {
constructor(private readonly configService: ConfigService) {}

public listProjects = async (): Promise<ProjectReferenceEntity[]> => {
const projects = getCollection<ProjectReferenceEntity>(
this.dataModelsPath,
"projects-v2",
"list.json",
);

if (projects === 404) throw new Error("Projects list not found");

return projects;
};

private dataModelsPath = join(__dirname, "../../../data");
}
7 changes: 7 additions & 0 deletions api/src/data/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Model } from "@dzcode.io/models/dist/_base";
import { ProjectEntity } from "@dzcode.io/models/dist/project";
import { RepositoryEntity } from "@dzcode.io/models/dist/repository";

export interface ProjectReferenceEntity extends Model<ProjectEntity> {
repositories: Model<RepositoryEntity>[];
}
2 changes: 1 addition & 1 deletion api/src/github/service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { GeneralGithubQuery, ListContributorsResponse } from "./types";
describe("GithubService", () => {
const githubQuery: GeneralGithubQuery = {
owner: "test-owner",
repo: "test-repo",
repository: "test-repo",
path: "test/path",
};
const contributorsMock: ListContributorsResponse = [
Expand Down
27 changes: 16 additions & 11 deletions api/src/github/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export class GithubService {

public listContributors = async ({
owner,
repo,
repository,
path,
}: GeneralGithubQuery): Promise<GithubUser[]> => {
const commits = await this.fetchService.get<ListContributorsResponse>(
`${this.apiURL}/repos/${owner}/${repo}/commits`,
`${this.apiURL}/repos/${owner}/${repository}/commits`,
{
headers: this.githubToken ? { Authorization: `Token ${this.githubToken}` } : {},
params: { path, state: "all", per_page: 100 }, // eslint-disable-line camelcase
Expand Down Expand Up @@ -58,10 +58,10 @@ export class GithubService {

public listRepositoryIssues = async ({
owner,
repo,
repository,
}: GitHubListRepositoryIssuesInput): Promise<GithubIssue[]> => {
const issues = await this.fetchService.get<GithubIssue[]>(
`${this.apiURL}/repos/${owner}/${repo}/issues`,
`${this.apiURL}/repos/${owner}/${repository}/issues`,
{
headers: this.githubToken ? { Authorization: `Token ${this.githubToken}` } : {},
params: { sort: "updated", per_page: 100 }, // eslint-disable-line camelcase
Expand All @@ -72,28 +72,33 @@ export class GithubService {

public listRepositoryLanguages = async ({
owner,
repo,
repository,
}: GitHubListRepositoryLanguagesInput): Promise<string[]> => {
const languages = await this.fetchService.get<Record<string, number>>(
`${this.apiURL}/repos/${owner}/${repo}/languages`,
`${this.apiURL}/repos/${owner}/${repository}/languages`,
{ headers: this.githubToken ? { Authorization: `Token ${this.githubToken}` } : {} },
);
return Object.keys(languages);
};

public listRepositoryContributors = async ({
owner,
repo,
repository,
}: Omit<GeneralGithubQuery, "path">): Promise<ListRepositoryContributorsResponse> => {
const contributors = await this.fetchService.get<ListRepositoryContributorsResponse>(
`${this.apiURL}/repos/${owner}/${repo}/contributors`,
`${this.apiURL}/repos/${owner}/${repository}/contributors`,
{
headers: this.githubToken ? { Authorization: `Token ${this.githubToken}` } : {},
params: { state: "all", per_page: 100 }, // eslint-disable-line camelcase
},
);

return contributors.filter(({ type }) => type === "User");
return (
contributors
// @TODO-ZM: filter out bots
.filter(({ type }) => type === "User")
.sort((a, b) => b.contributions - a.contributions)
);
};

public getRateLimit = async (): Promise<{ limit: number; used: number; ratio: number }> => {
Expand All @@ -111,10 +116,10 @@ export class GithubService {

public listRepositoryMilestones = async ({
owner,
repo,
repository,
}: GitHubListRepositoryMilestonesInput): Promise<GithubMilestone[]> => {
const milestones = await this.fetchService.get<GithubMilestone[]>(
`${this.apiURL}/repos/${owner}/${repo}/milestones`,
`${this.apiURL}/repos/${owner}/${repository}/milestones`,
{
headers: this.githubToken ? { Authorization: `Token ${this.githubToken}` } : {},
params: { state: "all", per_page: 100 }, // eslint-disable-line camelcase
Expand Down
4 changes: 2 additions & 2 deletions api/src/github/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type ListRepositoryContributorsResponse = Array<GithubUser & { contributi

export interface GeneralGithubQuery {
owner: string;
repo: string;
repository: string;
path: string;
}

Expand Down Expand Up @@ -55,7 +55,7 @@ export interface GitHubUserApiResponse {

export interface GitHubListRepositoryIssuesInput {
owner: string;
repo: string;
repository: string;
}

export type GitHubListRepositoryLanguagesInput = GitHubListRepositoryIssuesInput;
Expand Down
4 changes: 2 additions & 2 deletions api/src/milestone/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ export class MilestoneController {

@Get("/dzcode")
@OpenAPI({
summary: "Return a list of milestones for all dzcode.io repo",
summary: "Return a list of milestones for dzcode.io repository",
})
@ResponseSchema(GetMilestonesResponseDto)
public async getMilestones(): Promise<GetMilestonesResponseDto> {
const githubMilestones = await this.githubService.listRepositoryMilestones({
owner: "dzcode-io",
repo: "dzcode.io",
repository: "dzcode.io",
});
return {
milestones: githubMilestones
Expand Down
68 changes: 68 additions & 0 deletions api/src/project/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Controller, Get } from "routing-controllers";
import { OpenAPI, ResponseSchema } from "routing-controllers-openapi";
import { DataService } from "src/data/service";
import { GithubService } from "src/github/service";
import { Service } from "typedi";

import { GetProjectsResponseDto } from "./types";

@Service()
@Controller("/Projects")
export class ProjectController {
constructor(
private readonly githubService: GithubService,
private readonly dataService: DataService,
) {}

@Get("/")
@OpenAPI({
summary: "Return all projects listed in dzcode.io website",
})
@ResponseSchema(GetProjectsResponseDto)
public async getProjects(): Promise<GetProjectsResponseDto> {
// get projects from /data folder:
const projects = await this.dataService.listProjects();

// fetch info about these projects from github:
const infoRichProjects: GetProjectsResponseDto["projects"] = await Promise.all(
projects.map(async (project) => {
const { repositories } = project;
return {
...project,
repositories: await Promise.all(
repositories.map(async ({ provider, owner, repository }) => {
return {
provider,
owner,
repository,
stats: {
contributionCount: (
await this.githubService.listRepositoryIssues({ owner, repository })
).length,
languages: await this.githubService.listRepositoryLanguages({
owner,
repository,
}),
},
contributors: (
await this.githubService.listRepositoryContributors({
owner,
repository,
})
).map((contributor) => ({
id: `github/${contributor.id}`,
username: contributor.login,
avatarUrl: contributor.avatar_url,
})),
};
}),
),
};
}),
);

return {
projects: infoRichProjects,
};
}
}
7 changes: 6 additions & 1 deletion api/src/project/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Model } from "@dzcode.io/models/dist/_base";
import { ProjectEntity } from "@dzcode.io/models/dist/project";
import { RepositoryEntity } from "@dzcode.io/models/dist/repository";
import { Type } from "class-transformer";
import { ValidateNested } from "class-validator";
import { GeneralResponseDto } from "src/app/types";

export class GetProjectsResponseDto extends GeneralResponseDto {
@ValidateNested({ each: true })
@Type(() => ProjectEntity)
projects!: Model<ProjectEntity, "repositories">[];
// @TODO-ZM: find a way to DRY this, eg:
// projects!: Model<ProjectEntity, 'repositories' | 'repositories.contributors' | 'repositories.stats'>[]
projects!: Array<
Model<ProjectEntity> & { repositories: Model<RepositoryEntity, "contributors" | "stats">[] }
>;
}
25 changes: 9 additions & 16 deletions api/src/team/repository.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
import { getCollection } from "@dzcode.io/data/dist/get/collection";
import { Model } from "@dzcode.io/models/dist/_base";
import { ContributorEntity } from "@dzcode.io/models/dist/contributor";
import { ProjectReferenceEntity } from "@dzcode.io/models/dist/project-reference";
import { RepositoryEntity } from "@dzcode.io/models/dist/repository";
import { join } from "path";
import { DataService } from "src/data/service";
import { GithubService } from "src/github/service";
import { Service } from "typedi";

@Service()
export class TeamRepository {
constructor(private readonly githubService: GithubService) {
const projects = getCollection<Model<ProjectReferenceEntity, "repositories">>(
join(__dirname, "../../../data"),
"projects-v2",
"list.json",
);
this.projects = projects !== 404 ? projects : [];
}

private projects: Model<ProjectReferenceEntity, "repositories">[];
constructor(
private readonly githubService: GithubService,
private readonly dataService: DataService,
) {}

public async find(): Promise<Model<ContributorEntity, "repositories">[]> {
const projects = await this.dataService.listProjects();

// flatten repositories into one array
const repositories = this.projects.reduce<RepositoryEntity[]>(
const repositories = projects.reduce<RepositoryEntity[]>(
(repositories, project) => [...repositories, ...project.repositories],
[],
);
Expand All @@ -38,9 +32,8 @@ export class TeamRepository {
repositories.map(async ({ provider, owner, repository }) => {
const committers = await this.githubService.listRepositoryContributors({
owner,
repo: repository,
repository,
});
// @TODO-ZM: filter out bots
committers.forEach(({ avatar_url: avatarUrl, id, login, contributions }) => {
const uuid = `${provider}/${id}`;
// add new contributor if doesn't exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`should match snapshot when providing all fields: errors 1`] = `Array []
exports[`should match snapshot when providing all fields: output 1`] = `
ContributorEntity {
"avatarUrl": "https://avatars.githubusercontent.com/u/20110076?v=4",
"id": "20110076",
"id": "github/20110076",
"repositories": Array [],
"username": "ZibanPirate",
}
Expand All @@ -16,7 +16,7 @@ exports[`should match snapshot when providing required fields only: errors 1`] =
exports[`should match snapshot when providing required fields only: output 1`] = `
ContributorEntity {
"avatarUrl": "https://avatars.githubusercontent.com/u/20110076?v=4",
"id": "20110076",
"id": "github/20110076",
"username": "ZibanPirate",
}
`;
Expand Down
Loading

0 comments on commit 309d0c1

Please sign in to comment.