Skip to content

Commit c764268

Browse files
authored
Merge pull request #610 from dzcode-io/profile-page
feat: contributor page
2 parents 09ce41c + 554e7db commit c764268

File tree

26 files changed

+1374
-1762
lines changed

26 files changed

+1374
-1762
lines changed

api/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
"@types/body-parser": "^1.19.0",
3838
"@types/cors": "^2.8.9",
3939
"@types/express": "^4.17.9",
40-
"@types/express-rate-limit": "^6.0.0",
4140
"@types/fs-extra": "^11.0.4",
4241
"@types/lodash": "^4.17.7",
4342
"@types/make-fetch-happen": "^10.0.4",
@@ -77,9 +76,9 @@
7776
"lint:prettier": "prettier --config ../packages/tooling/.prettierrc --ignore-path ../packages/tooling/.prettierignore --log-level warn",
7877
"lint:ts-prune": "tsx ../packages/tooling/setup-ts-prune.ts && ts-prune --error",
7978
"lint:tsc": "tspc --noEmit",
80-
"start": "wait-port postgres:5432 && node dist/app/index.js",
79+
"start": "wait-port postgres:5432 && delay 2 && node dist/app/index.js",
8180
"start:dev": "tsx ../packages/tooling/nodemon.ts \"@dzcode.io/api\" && npm-run-all --parallel start:nodemon db:server",
82-
"start:nodemon": "wait-port localhost:5432 && nodemon dist/app/index.js",
81+
"start:nodemon": "wait-port localhost:5432 && delay 2 && nodemon dist/app/index.js",
8382
"test": "npm run build && npm run test:alone",
8483
"test:alone": "jest --config ../packages/tooling/jest.config.ts --rootDir .",
8584
"test:watch": "npm-run-all build --parallel build:watch \"test:alone --watch {@}\" --"

api/src/app/endpoints.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { GetContributionsResponse } from "src/contribution/types";
2-
import { GetContributorsResponse } from "src/contributor/types";
2+
import {
3+
GetContributorNameResponse,
4+
GetContributorResponse,
5+
GetContributorsResponse,
6+
} from "src/contributor/types";
37
import { GetMilestonesResponse } from "src/milestone/types";
48
import {
59
GetProjectNameResponse,
@@ -31,6 +35,14 @@ export interface Endpoints {
3135
"api:Contributors": {
3236
response: GetContributorsResponse;
3337
};
38+
"api:Contributors/:id": {
39+
response: GetContributorResponse;
40+
params: { id: string };
41+
};
42+
"api:contributors/:id/name": {
43+
response: GetContributorNameResponse;
44+
params: { id: string };
45+
};
3446
"api:MileStones/dzcode": {
3547
response: GetMilestonesResponse;
3648
};

api/src/contribution/repository.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ export class ContributionRepository {
3636
return camelCased;
3737
}
3838

39+
public async findForContributor(contributorId: string) {
40+
const statement = sql`
41+
SELECT
42+
${contributionsTable.id},
43+
${contributionsTable.title}
44+
FROM
45+
${contributionsTable}
46+
INNER JOIN
47+
${contributorsTable} ON ${contributionsTable.contributorId} = ${contributorsTable.id}
48+
WHERE
49+
${contributorsTable.id} = ${contributorId}
50+
ORDER BY
51+
${contributionsTable.updatedAt} DESC
52+
`;
53+
54+
const raw = await this.postgresService.db.execute(statement);
55+
const entries = Array.from(raw);
56+
const unStringifiedRaw = unStringifyDeep(entries);
57+
const camelCased = camelCaseObject(unStringifiedRaw);
58+
return camelCased;
59+
}
60+
3961
public async upsert(contribution: ContributionRow) {
4062
return await this.postgresService.db
4163
.insert(contributionsTable)

api/src/contributor/controller.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1-
import { Controller, Get } from "routing-controllers";
1+
import { Controller, Get, NotFoundError, Param } from "routing-controllers";
22
import { Service } from "typedi";
33

44
import { ContributorRepository } from "./repository";
5-
import { GetContributorsResponse } from "./types";
5+
import {
6+
GetContributorNameResponse,
7+
GetContributorResponse,
8+
GetContributorsResponse,
9+
} from "./types";
10+
import { ProjectRepository } from "src/project/repository";
11+
import { ContributionRepository } from "src/contribution/repository";
612

713
@Service()
814
@Controller("/Contributors")
915
export class ContributorController {
10-
constructor(private readonly contributorRepository: ContributorRepository) {}
16+
constructor(
17+
private readonly contributorRepository: ContributorRepository,
18+
private readonly projectRepository: ProjectRepository,
19+
private readonly contributionRepository: ContributionRepository,
20+
) {}
1121

1222
@Get("/")
1323
public async getContributors(): Promise<GetContributorsResponse> {
@@ -17,4 +27,34 @@ export class ContributorController {
1727
contributors,
1828
};
1929
}
30+
31+
@Get("/:id")
32+
public async getContributor(@Param("id") id: string): Promise<GetContributorResponse> {
33+
const [contributor, projects, contributions] = await Promise.all([
34+
this.contributorRepository.findWithStats(id),
35+
this.projectRepository.findForContributor(id),
36+
this.contributionRepository.findForContributor(id),
37+
]);
38+
39+
if (!contributor) throw new NotFoundError("Contributor not found");
40+
41+
return {
42+
contributor: {
43+
...contributor,
44+
projects,
45+
contributions,
46+
},
47+
};
48+
}
49+
50+
@Get("/:id/name")
51+
public async getContributorName(@Param("id") id: string): Promise<GetContributorNameResponse> {
52+
const contributor = await this.contributorRepository.findName(id);
53+
54+
if (!contributor) throw new NotFoundError("Contributor not found");
55+
56+
return {
57+
contributor,
58+
};
59+
}
2060
}

api/src/contributor/repository.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ import {
1616
export class ContributorRepository {
1717
constructor(private readonly postgresService: PostgresService) {}
1818

19+
public async findName(contributorId: string) {
20+
const statement = sql`
21+
SELECT
22+
${contributorsTable.id},
23+
${contributorsTable.name}
24+
FROM
25+
${contributorsTable}
26+
WHERE
27+
${contributorsTable.id} = ${contributorId}
28+
`;
29+
30+
const raw = await this.postgresService.db.execute(statement);
31+
const entries = Array.from(raw);
32+
const entry = entries[0];
33+
34+
if (!entry) return null;
35+
36+
const unStringifiedRaw = unStringifyDeep(entry);
37+
const camelCased = camelCaseObject(unStringifiedRaw);
38+
return camelCased;
39+
}
40+
1941
public async findForProject(projectId: string) {
2042
const statement = sql`
2143
SELECT
@@ -50,7 +72,9 @@ export class ContributorRepository {
5072
${contributorsTable.id},
5173
${contributorsTable.name},
5274
${contributorsTable.avatarUrl},
53-
sum(${contributorRepositoryRelationTable.score}) as ranking
75+
sum(${contributorRepositoryRelationTable.score}) as total_contribution_score,
76+
count(DISTINCT ${contributorRepositoryRelationTable.repositoryId}) as total_repository_count,
77+
(sum(${contributorRepositoryRelationTable.score}) * count(DISTINCT ${contributorRepositoryRelationTable.repositoryId})) as ranking
5478
FROM
5579
${contributorRepositoryRelationTable}
5680
JOIN
@@ -111,4 +135,35 @@ export class ContributorRepository {
111135
.delete(contributorsTable)
112136
.where(ne(contributorsTable.runId, runId));
113137
}
138+
139+
public async findWithStats(contributorId: string) {
140+
const statement = sql`
141+
SELECT
142+
${contributorsTable.id},
143+
${contributorsTable.name},
144+
${contributorsTable.avatarUrl},
145+
${contributorsTable.username},
146+
${contributorsTable.url},
147+
sum(${contributorRepositoryRelationTable.score}) as total_contribution_score,
148+
count(DISTINCT ${contributorRepositoryRelationTable.repositoryId}) as total_repository_count,
149+
(sum(${contributorRepositoryRelationTable.score}) * count(DISTINCT ${contributorRepositoryRelationTable.repositoryId})) as ranking
150+
FROM
151+
${contributorRepositoryRelationTable}
152+
JOIN
153+
${repositoriesTable} ON ${contributorRepositoryRelationTable.repositoryId} = ${repositoriesTable.id}
154+
JOIN
155+
${contributorsTable} ON ${contributorRepositoryRelationTable.contributorId} = ${contributorsTable.id}
156+
WHERE
157+
${contributorsTable.id} = ${contributorId}
158+
GROUP BY
159+
${contributorsTable.id}
160+
`;
161+
162+
const raw = await this.postgresService.db.execute(statement);
163+
const entries = Array.from(raw);
164+
const entry = entries[0];
165+
const unStringifiedRaw = unStringifyDeep(entry);
166+
const camelCased = camelCaseObject(unStringifiedRaw);
167+
return camelCased;
168+
}
114169
}

api/src/contributor/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
1+
import { ContributionEntity } from "@dzcode.io/models/dist/contribution";
12
import { ContributorEntity } from "@dzcode.io/models/dist/contributor";
3+
import { ProjectEntity } from "@dzcode.io/models/dist/project";
24
import { GeneralResponse } from "src/app/types";
35

46
export interface GetContributorsResponse extends GeneralResponse {
57
contributors: Array<
68
Pick<ContributorEntity, "id" | "name" | "avatarUrl"> & {
79
ranking: number;
10+
totalContributionScore: number;
11+
totalRepositoryCount: number;
812
}
913
>;
1014
}
15+
16+
export interface GetContributorResponse extends GeneralResponse {
17+
contributor: Omit<ContributorEntity, "runId"> & {
18+
ranking: number;
19+
totalContributionScore: number;
20+
totalRepositoryCount: number;
21+
projects: Array<
22+
Pick<ProjectEntity, "id" | "name"> & {
23+
totalRepoContributorCount: number;
24+
totalRepoScore: number;
25+
totalRepoStars: number;
26+
ranking: number;
27+
}
28+
>;
29+
contributions: Array<Pick<ContributionEntity, "id" | "title" | "type">>;
30+
};
31+
}
32+
33+
export interface GetContributorNameResponse extends GeneralResponse {
34+
contributor: Pick<ContributorEntity, "name">;
35+
}

api/src/digest/cron.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,16 @@ export class DigestCron {
145145
);
146146

147147
for (const repoContributor of repoContributorsFiltered) {
148-
const [{ id: contributorId }] = await this.contributorsRepository.upsert({
149-
name: repoContributor.name || repoContributor.login,
148+
const contributor = await this.githubService.getUser({
150149
username: repoContributor.login,
151-
url: repoContributor.html_url,
152-
avatarUrl: repoContributor.avatar_url,
150+
});
151+
const [{ id: contributorId }] = await this.contributorsRepository.upsert({
152+
name: contributor.name || contributor.login,
153+
username: contributor.login,
154+
url: contributor.html_url,
155+
avatarUrl: contributor.avatar_url,
153156
runId,
154-
id: `${provider}-${repoContributor.login}`,
157+
id: `${provider}-${contributor.login}`,
155158
});
156159

157160
await this.contributorsRepository.upsertRelationWithRepository({

api/src/github/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { GeneralResponse } from "src/app/types";
22

33
interface GithubUser {
44
login: string;
5-
name: string;
5+
name: string | null;
66
html_url: string;
77
avatar_url: string;
88
type: "User" | "_other";
99
}
1010

11-
interface GithubRepositoryContributor extends GithubUser {
11+
interface GithubRepositoryContributor extends Omit<GithubUser, "name"> {
1212
contributions: number;
1313
}
1414

api/src/project/repository.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,47 @@ export class ProjectRepository {
116116
return camelCased;
117117
}
118118

119+
public async findForContributor(id: string) {
120+
const statement = sql`
121+
SELECT
122+
id,
123+
name,
124+
sum(repo_with_stats.contributor_count)::int as total_repo_contributor_count,
125+
sum(repo_with_stats.stars)::int as total_repo_stars,
126+
sum(repo_with_stats.score)::int as total_repo_score,
127+
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
128+
FROM
129+
(
130+
SELECT
131+
repository_id,
132+
project_id,
133+
sum(score) as score,
134+
count(*) as contributor_count,
135+
stars
136+
FROM
137+
${contributorRepositoryRelationTable}
138+
JOIN
139+
${repositoriesTable} ON ${contributorRepositoryRelationTable.repositoryId} = ${repositoriesTable.id}
140+
WHERE
141+
${contributorRepositoryRelationTable.contributorId} = ${id}
142+
GROUP BY
143+
${contributorRepositoryRelationTable.repositoryId}, ${repositoriesTable.projectId}, ${repositoriesTable.stars}
144+
) as repo_with_stats
145+
JOIN
146+
${projectsTable} ON ${projectsTable.id} = repo_with_stats.project_id
147+
GROUP BY
148+
${projectsTable.id}
149+
ORDER BY
150+
ranking DESC
151+
`;
152+
153+
const raw = await this.postgresService.db.execute(statement);
154+
const entries = Array.from(raw);
155+
const unStringifiedRaw = unStringifyDeep(entries);
156+
const camelCased = camelCaseObject(unStringifiedRaw);
157+
return camelCased;
158+
}
159+
119160
public async findForSitemap() {
120161
const statement = sql`
121162
SELECT

0 commit comments

Comments
 (0)