Skip to content
5 changes: 2 additions & 3 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@types/body-parser": "^1.19.0",
"@types/cors": "^2.8.9",
"@types/express": "^4.17.9",
"@types/express-rate-limit": "^6.0.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.7",
"@types/make-fetch-happen": "^10.0.4",
Expand Down Expand Up @@ -77,9 +76,9 @@
"lint:prettier": "prettier --config ../packages/tooling/.prettierrc --ignore-path ../packages/tooling/.prettierignore --log-level warn",
"lint:ts-prune": "tsx ../packages/tooling/setup-ts-prune.ts && ts-prune --error",
"lint:tsc": "tspc --noEmit",
"start": "wait-port postgres:5432 && node dist/app/index.js",
"start": "wait-port postgres:5432 && delay 2 && node dist/app/index.js",
"start:dev": "tsx ../packages/tooling/nodemon.ts \"@dzcode.io/api\" && npm-run-all --parallel start:nodemon db:server",
"start:nodemon": "wait-port localhost:5432 && nodemon dist/app/index.js",
"start:nodemon": "wait-port localhost:5432 && delay 2 && nodemon dist/app/index.js",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need delay here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on my M1, postgres server is up, but you can't query it until few seconds after, so wait-port alone is not enough.

i only added this delay for the dev script start:nodemon

"test": "npm run build && npm run test:alone",
"test:alone": "jest --config ../packages/tooling/jest.config.ts --rootDir .",
"test:watch": "npm-run-all build --parallel build:watch \"test:alone --watch {@}\" --"
Expand Down
14 changes: 13 additions & 1 deletion api/src/app/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { GetContributionsResponse } from "src/contribution/types";
import { GetContributorsResponse } from "src/contributor/types";
import {
GetContributorNameResponse,
GetContributorResponse,
GetContributorsResponse,
} from "src/contributor/types";
import { GetMilestonesResponse } from "src/milestone/types";
import {
GetProjectNameResponse,
Expand Down Expand Up @@ -31,6 +35,14 @@ export interface Endpoints {
"api:Contributors": {
response: GetContributorsResponse;
};
"api:Contributors/:id": {
response: GetContributorResponse;
params: { id: string };
};
"api:contributors/:id/name": {
response: GetContributorNameResponse;
params: { id: string };
};
"api:MileStones/dzcode": {
response: GetMilestonesResponse;
};
Expand Down
22 changes: 22 additions & 0 deletions api/src/contribution/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ export class ContributionRepository {
return camelCased;
}

public async findForContributor(contributorId: string) {
const statement = sql`
SELECT
${contributionsTable.id},
${contributionsTable.title}
FROM
${contributionsTable}
INNER JOIN
${contributorsTable} ON ${contributionsTable.contributorId} = ${contributorsTable.id}
WHERE
${contributorsTable.id} = ${contributorId}
ORDER BY
${contributionsTable.updatedAt} DESC
`;

const raw = await this.postgresService.db.execute(statement);
const entries = Array.from(raw);
const unStringifiedRaw = unStringifyDeep(entries);
const camelCased = camelCaseObject(unStringifiedRaw);
return camelCased;
}

public async upsert(contribution: ContributionRow) {
return await this.postgresService.db
.insert(contributionsTable)
Expand Down
46 changes: 43 additions & 3 deletions api/src/contributor/controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { Controller, Get } from "routing-controllers";
import { Controller, Get, NotFoundError, Param } from "routing-controllers";
import { Service } from "typedi";

import { ContributorRepository } from "./repository";
import { GetContributorsResponse } from "./types";
import {
GetContributorNameResponse,
GetContributorResponse,
GetContributorsResponse,
} from "./types";
import { ProjectRepository } from "src/project/repository";
import { ContributionRepository } from "src/contribution/repository";

@Service()
@Controller("/Contributors")
export class ContributorController {
constructor(private readonly contributorRepository: ContributorRepository) {}
constructor(
private readonly contributorRepository: ContributorRepository,
private readonly projectRepository: ProjectRepository,
private readonly contributionRepository: ContributionRepository,
) {}

@Get("/")
public async getContributors(): Promise<GetContributorsResponse> {
Expand All @@ -17,4 +27,34 @@ export class ContributorController {
contributors,
};
}

@Get("/:id")
public async getContributor(@Param("id") id: string): Promise<GetContributorResponse> {
const [contributor, projects, contributions] = await Promise.all([
this.contributorRepository.findWithStats(id),
this.projectRepository.findForContributor(id),
this.contributionRepository.findForContributor(id),
]);

if (!contributor) throw new NotFoundError("Contributor not found");

return {
contributor: {
...contributor,
projects,
contributions,
},
};
}

@Get("/:id/name")
public async getContributorName(@Param("id") id: string): Promise<GetContributorNameResponse> {
const contributor = await this.contributorRepository.findName(id);

if (!contributor) throw new NotFoundError("Contributor not found");

return {
contributor,
};
}
}
57 changes: 56 additions & 1 deletion api/src/contributor/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ import {
export class ContributorRepository {
constructor(private readonly postgresService: PostgresService) {}

public async findName(contributorId: string) {
const statement = sql`
SELECT
${contributorsTable.id},
${contributorsTable.name}
FROM
${contributorsTable}
WHERE
${contributorsTable.id} = ${contributorId}
`;

const raw = await this.postgresService.db.execute(statement);
const entries = Array.from(raw);
const entry = entries[0];

if (!entry) return null;

const unStringifiedRaw = unStringifyDeep(entry);
const camelCased = camelCaseObject(unStringifiedRaw);
return camelCased;
}

public async findForProject(projectId: string) {
const statement = sql`
SELECT
Expand Down Expand Up @@ -50,7 +72,9 @@ export class ContributorRepository {
${contributorsTable.id},
${contributorsTable.name},
${contributorsTable.avatarUrl},
sum(${contributorRepositoryRelationTable.score}) as ranking
sum(${contributorRepositoryRelationTable.score}) as total_contribution_score,
count(DISTINCT ${contributorRepositoryRelationTable.repositoryId}) as total_repository_count,
(sum(${contributorRepositoryRelationTable.score}) * count(DISTINCT ${contributorRepositoryRelationTable.repositoryId})) as ranking
FROM
${contributorRepositoryRelationTable}
JOIN
Expand Down Expand Up @@ -111,4 +135,35 @@ export class ContributorRepository {
.delete(contributorsTable)
.where(ne(contributorsTable.runId, runId));
}

public async findWithStats(contributorId: string) {
const statement = sql`
SELECT
${contributorsTable.id},
${contributorsTable.name},
${contributorsTable.avatarUrl},
${contributorsTable.username},
${contributorsTable.url},
sum(${contributorRepositoryRelationTable.score}) as total_contribution_score,
count(DISTINCT ${contributorRepositoryRelationTable.repositoryId}) as total_repository_count,
(sum(${contributorRepositoryRelationTable.score}) * count(DISTINCT ${contributorRepositoryRelationTable.repositoryId})) as ranking
FROM
${contributorRepositoryRelationTable}
JOIN
${repositoriesTable} ON ${contributorRepositoryRelationTable.repositoryId} = ${repositoriesTable.id}
JOIN
${contributorsTable} ON ${contributorRepositoryRelationTable.contributorId} = ${contributorsTable.id}
WHERE
${contributorsTable.id} = ${contributorId}
GROUP BY
${contributorsTable.id}
`;

const raw = await this.postgresService.db.execute(statement);
const entries = Array.from(raw);
const entry = entries[0];
const unStringifiedRaw = unStringifyDeep(entry);
const camelCased = camelCaseObject(unStringifiedRaw);
return camelCased;
}
}
25 changes: 25 additions & 0 deletions api/src/contributor/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
import { ContributionEntity } from "@dzcode.io/models/dist/contribution";
import { ContributorEntity } from "@dzcode.io/models/dist/contributor";
import { ProjectEntity } from "@dzcode.io/models/dist/project";
import { GeneralResponse } from "src/app/types";

export interface GetContributorsResponse extends GeneralResponse {
contributors: Array<
Pick<ContributorEntity, "id" | "name" | "avatarUrl"> & {
ranking: number;
totalContributionScore: number;
totalRepositoryCount: number;
}
>;
}

export interface GetContributorResponse extends GeneralResponse {
contributor: Omit<ContributorEntity, "runId"> & {
ranking: number;
totalContributionScore: number;
totalRepositoryCount: number;
projects: Array<
Pick<ProjectEntity, "id" | "name"> & {
totalRepoContributorCount: number;
totalRepoScore: number;
totalRepoStars: number;
ranking: number;
}
>;
contributions: Array<Pick<ContributionEntity, "id" | "title" | "type">>;
};
}

export interface GetContributorNameResponse extends GeneralResponse {
contributor: Pick<ContributorEntity, "name">;
}
13 changes: 8 additions & 5 deletions api/src/digest/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,16 @@ export class DigestCron {
);

for (const repoContributor of repoContributorsFiltered) {
const [{ id: contributorId }] = await this.contributorsRepository.upsert({
name: repoContributor.name || repoContributor.login,
const contributor = await this.githubService.getUser({
username: repoContributor.login,
url: repoContributor.html_url,
avatarUrl: repoContributor.avatar_url,
});
const [{ id: contributorId }] = await this.contributorsRepository.upsert({
name: contributor.name || contributor.login,
username: contributor.login,
url: contributor.html_url,
avatarUrl: contributor.avatar_url,
runId,
id: `${provider}-${repoContributor.login}`,
id: `${provider}-${contributor.login}`,
});

await this.contributorsRepository.upsertRelationWithRepository({
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 @@ -2,13 +2,13 @@ import { GeneralResponse } from "src/app/types";

interface GithubUser {
login: string;
name: string;
name: string | null;
html_url: string;
avatar_url: string;
type: "User" | "_other";
}

interface GithubRepositoryContributor extends GithubUser {
interface GithubRepositoryContributor extends Omit<GithubUser, "name"> {
contributions: number;
}

Expand Down
41 changes: 41 additions & 0 deletions api/src/project/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,47 @@ export class ProjectRepository {
return camelCased;
}

public async findForContributor(id: string) {
const statement = sql`
SELECT
id,
name,
sum(repo_with_stats.contributor_count)::int as total_repo_contributor_count,
sum(repo_with_stats.stars)::int as total_repo_stars,
sum(repo_with_stats.score)::int as total_repo_score,
ROUND( 100 * sum(repo_with_stats.contributor_count) + 100 * sum(repo_with_stats.stars) + max(repo_with_stats.score) - sum(repo_with_stats.score) / sum(repo_with_stats.contributor_count) )::int as ranking
FROM
(
SELECT
repository_id,
project_id,
sum(score) as score,
count(*) as contributor_count,
stars
FROM
${contributorRepositoryRelationTable}
JOIN
${repositoriesTable} ON ${contributorRepositoryRelationTable.repositoryId} = ${repositoriesTable.id}
WHERE
${contributorRepositoryRelationTable.contributorId} = ${id}
GROUP BY
${contributorRepositoryRelationTable.repositoryId}, ${repositoriesTable.projectId}, ${repositoriesTable.stars}
) as repo_with_stats
JOIN
${projectsTable} ON ${projectsTable.id} = repo_with_stats.project_id
GROUP BY
${projectsTable.id}
ORDER BY
ranking DESC
`;

const raw = await this.postgresService.db.execute(statement);
const entries = Array.from(raw);
const unStringifiedRaw = unStringifyDeep(entries);
const camelCased = camelCaseObject(unStringifiedRaw);
return camelCased;
}

public async findForSitemap() {
const statement = sql`
SELECT
Expand Down
Loading
Loading