diff --git a/api/db/migrations/0001_black_eternals.sql b/api/db/migrations/0001_black_eternals.sql new file mode 100644 index 000000000..c3109ac28 --- /dev/null +++ b/api/db/migrations/0001_black_eternals.sql @@ -0,0 +1,10 @@ +CREATE TABLE `contributor_repository_relation` ( + `contributor_id` integer NOT NULL, + `repository_id` integer NOT NULL, + `record_imported_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `run_id` text DEFAULT 'initial-run-id' NOT NULL, + `score` integer NOT NULL, + PRIMARY KEY(`contributor_id`, `repository_id`), + FOREIGN KEY (`contributor_id`) REFERENCES `contributors`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action +); diff --git a/api/db/migrations/meta/0001_snapshot.json b/api/db/migrations/meta/0001_snapshot.json new file mode 100644 index 000000000..67f027972 --- /dev/null +++ b/api/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,386 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d99b3c61-9212-4dde-814c-d4cbac5ea54d", + "prevId": "ba41012f-4495-42ff-ace1-61bd7eaef476", + "tables": { + "contributions": { + "name": "contributions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "activity_count": { + "name": "activity_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contributor_id": { + "name": "contributor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "contributions_url_unique": { + "name": "contributions_url_unique", + "columns": ["url"], + "isUnique": true + } + }, + "foreignKeys": { + "contributions_repository_id_repositories_id_fk": { + "name": "contributions_repository_id_repositories_id_fk", + "tableFrom": "contributions", + "tableTo": "repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributions_contributor_id_contributors_id_fk": { + "name": "contributions_contributor_id_contributors_id_fk", + "tableFrom": "contributions", + "tableTo": "contributors", + "columnsFrom": ["contributor_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "contributor_repository_relation": { + "name": "contributor_repository_relation", + "columns": { + "contributor_id": { + "name": "contributor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'initial-run-id'" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "contributor_repository_relation_contributor_id_contributors_id_fk": { + "name": "contributor_repository_relation_contributor_id_contributors_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "contributors", + "columnsFrom": ["contributor_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributor_repository_relation_repository_id_repositories_id_fk": { + "name": "contributor_repository_relation_repository_id_repositories_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contributor_repository_relation_pk": { + "columns": ["contributor_id", "repository_id"], + "name": "contributor_repository_relation_pk" + } + }, + "uniqueConstraints": {} + }, + "contributors": { + "name": "contributors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'initial-run-id'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "contributors_url_unique": { + "name": "contributors_url_unique", + "columns": ["url"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'initial-run-id'" + } + }, + "indexes": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "columns": ["slug"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "repositories": { + "name": "repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'initial-run-id'" + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "repositories_provider_owner_name_unique": { + "name": "repositories_provider_owner_name_unique", + "columns": ["provider", "owner", "name"], + "isUnique": true + } + }, + "foreignKeys": { + "repositories_project_id_projects_id_fk": { + "name": "repositories_project_id_projects_id_fk", + "tableFrom": "repositories", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/api/db/migrations/meta/_journal.json b/api/db/migrations/meta/_journal.json index 434d06b88..7da2d65ac 100644 --- a/api/db/migrations/meta/_journal.json +++ b/api/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1725654548149, "tag": "0000_sudden_doctor_doom", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1725790243766, + "tag": "0001_black_eternals", + "breakpoints": true } ] } diff --git a/api/src/_test/mocks.ts b/api/src/_test/mocks.ts index 34c50b2a5..a96d5fb81 100644 --- a/api/src/_test/mocks.ts +++ b/api/src/_test/mocks.ts @@ -3,35 +3,8 @@ import { GithubUser } from "src/github/types"; export const githubUserMock: GithubUser = { login: "ZibanPirate", - id: 20110076, - node_id: "MDQ6VXNlcjIwMTEwMDc2", - avatar_url: "https://avatars.githubusercontent.com/u/20110076?v=4", - gravatar_id: "", - url: "https://api.github.com/users/ZibanPirate", html_url: "https://github.com/ZibanPirate", - followers_url: "https://api.github.com/users/ZibanPirate/followers", - following_url: "https://api.github.com/users/ZibanPirate/following{/other_user}", - gists_url: "https://api.github.com/users/ZibanPirate/gists{/gist_id}", - starred_url: "https://api.github.com/users/ZibanPirate/starred{/owner}{/repo}", - subscriptions_url: "https://api.github.com/users/ZibanPirate/subscriptions", - organizations_url: "https://api.github.com/users/ZibanPirate/orgs", - repos_url: "https://api.github.com/users/ZibanPirate/repos", - events_url: "https://api.github.com/users/ZibanPirate/events{/privacy}", - received_events_url: "https://api.github.com/users/ZibanPirate/received_events", - type: "User", - site_admin: false, + avatar_url: "https://avatars.githubusercontent.com/u/20110076?v=4", name: "Zakaria Mansouri", - company: "@dzcode-io @avimedical", - blog: "zak.dzcode.io", - location: "Algeria", - email: "", - hireable: true, - bio: "One-man-army lone programmer", - twitter_username: "ZibanPirate", - public_repos: 18, - public_gists: 2, - followers: 130, - following: 92, - created_at: "2016-06-23T12:41:14Z", - updated_at: "2023-04-10T21:31:26Z", + type: "User", }; diff --git a/api/src/app/endpoints.ts b/api/src/app/endpoints.ts index 0cf5e8fa6..0ced0b68c 100644 --- a/api/src/app/endpoints.ts +++ b/api/src/app/endpoints.ts @@ -1,9 +1,9 @@ import { GetArticleResponseDto, GetArticlesResponseDto } from "src/article/types"; import { GetContributionsResponseDto } from "src/contribution/types"; +import { GetContributorsResponseDto } from "src/contributor/types"; import { GetADocumentationResponseDto, GetDocumentationResponseDto } from "src/documentation/types"; import { GetMilestonesResponseDto } from "src/milestone/types"; import { GetProjectsResponseDto } from "src/project/types"; -import { GetTeamResponseDto } from "src/team/types"; // ts-prune-ignore-next export interface Endpoints { @@ -26,10 +26,9 @@ export interface Endpoints { }; "api:Contributions": { response: GetContributionsResponseDto; - query: [string, string][]; }; - "api:Team": { - response: GetTeamResponseDto; + "api:Contributors": { + response: GetContributorsResponseDto; }; "api:MileStones/dzcode": { response: GetMilestonesResponseDto; diff --git a/api/src/app/index.ts b/api/src/app/index.ts index b3ae1f3d9..b829808cc 100644 --- a/api/src/app/index.ts +++ b/api/src/app/index.ts @@ -9,6 +9,7 @@ import { createExpressServer, RoutingControllersOptions, useContainer } from "ro import { ArticleController } from "src/article/controller"; import { ConfigService } from "src/config/service"; import { ContributionController } from "src/contribution/controller"; +import { ContributorController } from "src/contributor/controller"; import { DigestCron } from "src/digest/cron"; import { DocumentationController } from "src/documentation/controller"; import { GithubController } from "src/github/controller"; @@ -47,6 +48,7 @@ export const routingControllersOptions: RoutingControllersOptions = { ProjectController, ArticleController, DocumentationController, + ContributorController, ], middlewares: [ SecurityMiddleware, diff --git a/api/src/article/controller.ts b/api/src/article/controller.ts index 295f269fe..77b94bcb0 100644 --- a/api/src/article/controller.ts +++ b/api/src/article/controller.ts @@ -7,6 +7,7 @@ import { Service } from "typedi"; import { GetArticleResponseDto, GetArticlesResponseDto } from "./types"; +// @TODO-ZM: remove article and learn controllers @Service() @Controller("/Articles") export class ArticleController { @@ -42,6 +43,8 @@ export class ArticleController { const authors = await Promise.all( article.authors.map(async (author) => { const githubUser = await this.githubService.getUser({ username: author }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore return this.githubService.githubUserToAccountEntity(githubUser); }), ); @@ -77,6 +80,8 @@ export class ArticleController { } }, []) .sort((a, b) => uniqUsernames[b.login] - uniqUsernames[a.login]) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .map((committer) => this.githubService.githubUserToAccountEntity(committer)) .filter(({ id }) => !authors.find((author) => author.id === id)); diff --git a/api/src/contribution/repository.ts b/api/src/contribution/repository.ts index 5010475c7..c0d7891c4 100644 --- a/api/src/contribution/repository.ts +++ b/api/src/contribution/repository.ts @@ -12,9 +12,7 @@ import { ContributionRow, contributionsTable } from "./table"; @Service() export class ContributionRepository { - constructor(private readonly sqliteService: SQLiteService) { - this.findForList(); - } + constructor(private readonly sqliteService: SQLiteService) {} public async upsert(contribution: ContributionRow) { return await this.sqliteService.db diff --git a/api/src/contribution/types.ts b/api/src/contribution/types.ts index bf80b2292..a2e3c5190 100644 --- a/api/src/contribution/types.ts +++ b/api/src/contribution/types.ts @@ -4,6 +4,7 @@ import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; import { GeneralResponseDto } from "src/app/types"; +// @TODO-ZM: remove "dto" from all interfaces export interface GetContributionsResponseDto extends GeneralResponseDto { contributions: Array< Pick & { diff --git a/api/src/contributor/controller.ts b/api/src/contributor/controller.ts new file mode 100644 index 000000000..60427a964 --- /dev/null +++ b/api/src/contributor/controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get } from "routing-controllers"; +import { Service } from "typedi"; + +import { ContributorRepository } from "./repository"; +import { GetContributorsResponseDto } from "./types"; + +@Service() +@Controller("/Contributors") +export class ContributorController { + constructor(private readonly contributorRepository: ContributorRepository) {} + + @Get("/") + public async getContributors(): Promise { + const contributors = await this.contributorRepository.findForList(); + + return { + contributors, + }; + } +} diff --git a/api/src/contributor/repository.ts b/api/src/contributor/repository.ts index 45ae3d382..2c010633f 100644 --- a/api/src/contributor/repository.ts +++ b/api/src/contributor/repository.ts @@ -1,13 +1,98 @@ -import { ne } from "drizzle-orm"; +import { ne, sql } from "drizzle-orm"; +import { camelCaseObject } from "src/_utils/case"; +import { unStringifyDeep } from "src/_utils/unstringify-deep"; +import { projectsTable } from "src/project/table"; +import { repositoriesTable } from "src/repository/table"; import { SQLiteService } from "src/sqlite/service"; import { Service } from "typedi"; -import { ContributorRow, contributorsTable } from "./table"; +import { + ContributorRepositoryRelationRow, + contributorRepositoryRelationTable, + ContributorRow, + contributorsTable, +} from "./table"; @Service() export class ContributorRepository { constructor(private readonly sqliteService: SQLiteService) {} + public async findForList() { + const statement = sql` + SELECT + sum(c.score) as score, + cr.id as id, + cr.name as name, + cr.username as username, + cr.url as url, + cr.avatar_url as avatar_url, + json_group_array( + json_object( + 'id', + p.id, + 'name', + p.name, + 'score', + c.score, + 'repositories', + c.repositories + ) + ) AS projects + FROM + (SELECT + sum(crr.score) as score, + crr.contributor_id as contributor_id, + crr.project_id as project_id, + json_group_array( + json_object( + 'id', + r.id, + 'owner', + r.owner, + 'name', + r.name, + 'score', + crr.score + ) + ) AS repositories + FROM + (SELECT + contributor_id, + repository_id, + score, + r.project_id as project_id + FROM + ${contributorRepositoryRelationTable} crr + INNER JOIN + ${repositoriesTable} r ON crr.repository_id = r.id + ORDER BY + crr.score DESC + ) as crr + INNER JOIN + ${repositoriesTable} r ON crr.repository_id = r.id + GROUP BY + crr.contributor_id, crr.project_id + ORDER BY + crr.score DESC + ) as c + INNER JOIN + ${contributorsTable} cr ON c.contributor_id = cr.id + INNER JOIN + ${projectsTable} p ON c.project_id = p.id + GROUP BY + c.contributor_id + ORDER BY + score DESC + `; + + const raw = this.sqliteService.db.all(statement); + const unStringifiedRaw = unStringifyDeep(raw); + + const camelCased = camelCaseObject(unStringifiedRaw); + + return camelCased; + } + public async upsert(contributor: ContributorRow) { return await this.sqliteService.db .insert(contributorsTable) @@ -19,6 +104,31 @@ export class ContributorRepository { .returning({ id: contributorsTable.id }); } + public async upsertRelationWithRepository( + contributorRelationWithRepository: ContributorRepositoryRelationRow, + ) { + return await this.sqliteService.db + .insert(contributorRepositoryRelationTable) + .values(contributorRelationWithRepository) + .onConflictDoUpdate({ + target: [ + contributorRepositoryRelationTable.contributorId, + contributorRepositoryRelationTable.repositoryId, + ], + set: contributorRelationWithRepository, + }) + .returning({ + contributorId: contributorRepositoryRelationTable.contributorId, + repositoryId: contributorRepositoryRelationTable.repositoryId, + }); + } + + public async deleteAllRelationWithRepositoryButWithRunId(runId: string) { + return await this.sqliteService.db + .delete(contributorRepositoryRelationTable) + .where(ne(contributorRepositoryRelationTable.runId, runId)); + } + public async deleteAllButWithRunId(runId: string) { return await this.sqliteService.db .delete(contributorsTable) diff --git a/api/src/contributor/table.ts b/api/src/contributor/table.ts index 72be1d5e1..571200fde 100644 --- a/api/src/contributor/table.ts +++ b/api/src/contributor/table.ts @@ -1,7 +1,8 @@ import { Model } from "@dzcode.io/models/dist/_base"; import { ContributorEntity } from "@dzcode.io/models/dist/contributor"; import { sql } from "drizzle-orm"; -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { repositoriesTable } from "src/repository/table"; export const contributorsTable = sqliteTable("contributors", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), @@ -18,3 +19,29 @@ export const contributorsTable = sqliteTable("contributors", { contributorsTable.$inferSelect satisfies Model; export type ContributorRow = typeof contributorsTable.$inferInsert; + +export const contributorRepositoryRelationTable = sqliteTable( + "contributor_repository_relation", + { + contributorId: integer("contributor_id") + .notNull() + .references(() => contributorsTable.id), + repositoryId: integer("repository_id") + .notNull() + .references(() => repositoriesTable.id), + recordImportedAt: text("record_imported_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + runId: text("run_id").notNull().default("initial-run-id"), + score: integer("score").notNull(), + }, + (table) => ({ + pk: primaryKey({ + name: "contributor_repository_relation_pk", + columns: [table.contributorId, table.repositoryId], + }), + }), +); + +export type ContributorRepositoryRelationRow = + typeof contributorRepositoryRelationTable.$inferInsert; diff --git a/api/src/contributor/types.ts b/api/src/contributor/types.ts new file mode 100644 index 000000000..1048b0f71 --- /dev/null +++ b/api/src/contributor/types.ts @@ -0,0 +1,17 @@ +import { ContributorEntity } from "@dzcode.io/models/dist/contributor"; +import { ProjectEntity } from "@dzcode.io/models/dist/project"; +import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; +import { GeneralResponseDto } from "src/app/types"; + +export interface GetContributorsResponseDto extends GeneralResponseDto { + contributors: Array< + Pick & { + projects: Array< + Pick & { + repositories: Array>; + } + >; + score: number; + } + >; +} diff --git a/api/src/digest/cron.ts b/api/src/digest/cron.ts index 037999264..754bda438 100644 --- a/api/src/digest/cron.ts +++ b/api/src/digest/cron.ts @@ -97,6 +97,9 @@ export class DigestCron { for (const issue of issues.issues) { const githubUser = await this.githubService.getUser({ username: issue.user.login }); + + if (githubUser.type !== "User") continue; + const [{ id: contributorId }] = await this.contributorsRepository.upsert({ name: githubUser.name || githubUser.login, username: githubUser.login, @@ -105,6 +108,13 @@ export class DigestCron { runId, }); + await this.contributorsRepository.upsertRelationWithRepository({ + contributorId, + repositoryId, + runId, + score: 1, + }); + const type = issue.pull_request ? "PULL_REQUEST" : "ISSUE"; const [{ id: contributionId }] = await this.contributionsRepository.upsert({ title: issue.title, @@ -119,6 +129,32 @@ export class DigestCron { console.log("contributionId", contributionId); } + + const repoContributors = await this.githubService.listRepositoryContributors({ + owner: repository.owner, + repository: repository.name, + }); + + const repoContributorsFiltered = repoContributors.filter( + (contributor) => contributor.type === "User", + ); + + for (const repoContributor of repoContributorsFiltered) { + const [{ id: contributorId }] = await this.contributorsRepository.upsert({ + name: repoContributor.name || repoContributor.login, + username: repoContributor.login, + url: repoContributor.html_url, + avatarUrl: repoContributor.avatar_url, + runId, + }); + + await this.contributorsRepository.upsertRelationWithRepository({ + contributorId, + repositoryId, + runId, + score: repoContributor.contributions, + }); + } } catch (error) { // @TODO-ZM: capture error console.error(error); @@ -135,10 +171,16 @@ export class DigestCron { } } - await this.contributorsRepository.deleteAllButWithRunId(runId); - await this.contributionsRepository.deleteAllButWithRunId(runId); - await this.repositoriesRepository.deleteAllButWithRunId(runId); - await this.projectsRepository.deleteAllButWithRunId(runId); + try { + await this.contributorsRepository.deleteAllRelationWithRepositoryButWithRunId(runId); + await this.contributorsRepository.deleteAllButWithRunId(runId); + await this.contributionsRepository.deleteAllButWithRunId(runId); + await this.repositoriesRepository.deleteAllButWithRunId(runId); + await this.projectsRepository.deleteAllButWithRunId(runId); + } catch (error) { + // @TODO-ZM: capture error + console.error(error); + } this.logger.info({ message: `Digest cron finished, runId: ${runId}` }); } diff --git a/api/src/documentation/controller.ts b/api/src/documentation/controller.ts index 06fcf3708..51f823396 100644 --- a/api/src/documentation/controller.ts +++ b/api/src/documentation/controller.ts @@ -44,6 +44,8 @@ export class DocumentationController { const authors = await Promise.all( documentation.authors.map(async (author) => { const githubUser = await this.githubService.getUser({ username: author }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore return this.githubService.githubUserToAccountEntity(githubUser); }), ); @@ -79,6 +81,8 @@ export class DocumentationController { } }, []) .sort((a, b) => uniqUsernames[b.login] - uniqUsernames[a.login]) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .map((contributor) => this.githubService.githubUserToAccountEntity(contributor)) .filter(({ id }) => !authors.find((author) => author.id === id)); diff --git a/api/src/github/service.ts b/api/src/github/service.ts index 45b8fb8cb..fbaf173a2 100644 --- a/api/src/github/service.ts +++ b/api/src/github/service.ts @@ -45,6 +45,8 @@ export class GithubService { const contributors = commits // @TODO-ZM: dry to a user block-list // excluding github.com/web-flow user + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .filter((item) => item.committer && item.committer.id !== 19864447) .map(({ committer }) => committer); return contributors; @@ -116,12 +118,7 @@ export class GithubService { // @TODO-ZM: validate responses using DTOs, for all fetchService methods if (!Array.isArray(contributors)) return []; - return ( - contributors - // @TODO-ZM: filter out bots - .filter(({ type }) => type === "User") - .sort((a, b) => b.contributions - a.contributions) - ); + return contributors; }; public getRateLimit = async (): Promise<{ limit: number; used: number; ratio: number }> => { @@ -152,6 +149,8 @@ export class GithubService { }; public githubUserToAccountEntity = ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore user: Pick, ): Model => ({ id: `github/${user.id}`, diff --git a/api/src/github/types.ts b/api/src/github/types.ts index d3e5fc77c..be2a28076 100644 --- a/api/src/github/types.ts +++ b/api/src/github/types.ts @@ -3,61 +3,13 @@ import { GeneralResponseDto } from "src/app/types"; export interface GithubUser { login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; - received_events_url: string; - type: string; - site_admin: boolean; name: string; - company: string; - blog: string; - location: string; - email: string; - hireable: boolean; - bio: string; - twitter_username: string; - public_repos: number; - public_gists: number; - followers: number; - following: number; - created_at: string; - updated_at: string; + html_url: string; + avatar_url: string; + type: "User" | "_other"; } -export interface GithubRepositoryContributor - extends Pick< - GithubUser, - | "login" - | "id" - | "node_id" - | "avatar_url" - | "gravatar_id" - | "url" - | "html_url" - | "followers_url" - | "following_url" - | "gists_url" - | "starred_url" - | "subscriptions_url" - | "organizations_url" - | "repos_url" - | "events_url" - | "received_events_url" - | "type" - | "site_admin" - > { +export interface GithubRepositoryContributor extends GithubUser { contributions: number; } diff --git a/api/src/team/repository.ts b/api/src/team/repository.ts index c72388575..df250abf0 100644 --- a/api/src/team/repository.ts +++ b/api/src/team/repository.ts @@ -43,6 +43,8 @@ export class TeamRepository { repository: name, }); contributors.forEach((contributor) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const uuid = this.githubService.githubUserToAccountEntity({ ...contributor, name: "", @@ -89,6 +91,8 @@ export class TeamRepository { .map(async (uuid) => { const { repositories, login } = contributorsUsernameRankedRecord[uuid]; const githubUser = await this.githubService.getUser({ username: login }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const account = this.githubService.githubUserToAccountEntity(githubUser); return { ...account, repositories }; diff --git a/packages/models/src/project-reference/__snapshots__/index.spec.ts.snap b/packages/models/src/project-reference/__snapshots__/index.spec.ts.snap index 42c998511..7f3643bbd 100644 --- a/packages/models/src/project-reference/__snapshots__/index.spec.ts.snap +++ b/packages/models/src/project-reference/__snapshots__/index.spec.ts.snap @@ -7,14 +7,14 @@ ProjectReferenceEntity { "name": "Leblad", "repositories": [ RepositoryReferenceEntity { + "name": "leblad", "owner": "dzcode-io", "provider": "github", - "repository": "leblad", }, RepositoryReferenceEntity { + "name": "leblad-py", "owner": "abderrahmaneMustapha", "provider": "github", - "repository": "leblad-py", }, ], "slug": "Leblad", @@ -28,14 +28,14 @@ ProjectReferenceEntity { "name": "Leblad", "repositories": [ RepositoryReferenceEntity { + "name": "leblad", "owner": "dzcode-io", "provider": "github", - "repository": "leblad", }, RepositoryReferenceEntity { + "name": "leblad-py", "owner": "abderrahmaneMustapha", "provider": "github", - "repository": "leblad-py", }, ], "slug": "Leblad", diff --git a/packages/models/src/repository-reference/__snapshots__/index.spec.ts.snap b/packages/models/src/repository-reference/__snapshots__/index.spec.ts.snap index c9192cf93..15c39ca2f 100644 --- a/packages/models/src/repository-reference/__snapshots__/index.spec.ts.snap +++ b/packages/models/src/repository-reference/__snapshots__/index.spec.ts.snap @@ -6,9 +6,9 @@ exports[`should match snapshot when providing all fields: output 1`] = ` RepositoryReferenceEntity { "contributions": [], "contributors": [], + "name": "leblad", "owner": "dzcode-io", "provider": "github", - "repository": "leblad", } `; @@ -16,9 +16,9 @@ exports[`should match snapshot when providing required fields only: errors 1`] = exports[`should match snapshot when providing required fields only: output 1`] = ` RepositoryReferenceEntity { + "name": "leblad", "owner": "dzcode-io", "provider": "github", - "repository": "leblad", } `; @@ -45,9 +45,9 @@ exports[`should show an error that matches snapshot when passing empty object: e ValidationError { "children": [], "constraints": { - "isString": "repository must be a string", + "isString": "name must be a string", }, - "property": "repository", + "property": "name", "target": RepositoryReferenceEntity {}, "value": undefined, }, diff --git a/web/src/pages/team/index.tsx b/web/src/pages/team/index.tsx index 134a69bcf..df339866d 100644 --- a/web/src/pages/team/index.tsx +++ b/web/src/pages/team/index.tsx @@ -51,15 +51,18 @@ export default function Page(): JSX.Element { className="rounded-full w-20 h-20" />

{contributor.name}

-
    - {contributor.repositories.map((repository, repositoryIndex) => ( -
  • - - {getRepositoryName(repository)} - -
  • +
    + {contributor.projects.map((project, projectIndex) => ( +
    + {project.name} + {project.repositories.map((repository, repositoryIndex) => ( + + {getRepositoryName(repository)} + + ))} +
    ))} -
+ ))} diff --git a/web/src/redux/actions/contributions.ts b/web/src/redux/actions/contributions.ts index 642530343..cf9ae31f3 100644 --- a/web/src/redux/actions/contributions.ts +++ b/web/src/redux/actions/contributions.ts @@ -8,7 +8,7 @@ export const fetchContributionsListAction = (): ThunkAction => async (dispatch) => { try { dispatch(contributionsPageSlice.actions.set({ contributionsList: null })); - const { contributions } = await fetchV2("api:Contributions", { query: [] }); + const { contributions } = await fetchV2("api:Contributions", {}); dispatch(contributionsPageSlice.actions.set({ contributionsList: contributions })); } catch (error) { diff --git a/web/src/redux/actions/contributors.ts b/web/src/redux/actions/contributors.ts index 9224adac2..f3e050c53 100644 --- a/web/src/redux/actions/contributors.ts +++ b/web/src/redux/actions/contributors.ts @@ -8,7 +8,7 @@ export const fetchContributorsListAction = (): ThunkAction => async (dispatch) => { try { dispatch(contributorsPageSlice.actions.set({ contributorsList: null })); - const { contributors } = await fetchV2("api:Team", {}); + const { contributors } = await fetchV2("api:Contributors", {}); dispatch(contributorsPageSlice.actions.set({ contributorsList: contributors })); } catch (error) { dispatch(contributorsPageSlice.actions.set({ contributorsList: "ERROR" })); diff --git a/web/src/redux/slices/contributors-page.ts b/web/src/redux/slices/contributors-page.ts index 47150399a..554642480 100644 --- a/web/src/redux/slices/contributors-page.ts +++ b/web/src/redux/slices/contributors-page.ts @@ -1,11 +1,11 @@ -import { GetTeamResponseDto } from "@dzcode.io/api/dist/team/types"; +import { GetContributorsResponseDto } from "@dzcode.io/api/dist/contributor/types"; import { createSlice } from "@reduxjs/toolkit"; import { setReducerFactory } from "src/redux/utils"; import { Loadable } from "src/utils/loadable"; // ts-prune-ignore-next export interface ContributorsPageState { - contributorsList: Loadable; + contributorsList: Loadable; } const initialState: ContributorsPageState = {