diff --git a/packages/canyon-backend/schema.gql b/packages/canyon-backend/schema.gql index 41d21680..31f12e14 100644 --- a/packages/canyon-backend/schema.gql +++ b/packages/canyon-backend/schema.gql @@ -33,10 +33,24 @@ type ProjectPagesModel { total: Float! } +type ProjectChartDataModel { + """整体覆盖率""" + statements: Float! + + """New Lines""" + newlines: Float! + + """sha""" + sha: String! +} + type Query { """提供执行此查询的用户的详细信息(通过授权 Bearer 标头)""" me: User! """获取Project""" getProjects(current: Int!, pageSize: Int!, keyword: String!): ProjectPagesModel! + + """获取Project""" + getProjectChartData(projectID: String!): [ProjectChartDataModel!]! } \ No newline at end of file diff --git a/packages/canyon-backend/src/adapter/gitlab.adapter.ts b/packages/canyon-backend/src/adapter/gitlab.adapter.ts new file mode 100755 index 00000000..ac2a50ae --- /dev/null +++ b/packages/canyon-backend/src/adapter/gitlab.adapter.ts @@ -0,0 +1,183 @@ +import axios from 'axios'; + +interface FileInfo { + file_name: string; + file_path: string; + size: number; + encoding: string; + content_sha256: string; + ref: string; + blob_id: string; + commit_id: string; + last_commit_id: string; + content: string; +} + +interface Commit { + id: string; + short_id: string; + created_at: string; + parent_ids: string[]; + title: string; + message: string; + author_name: string; + author_email: string; + authored_date: string; + committer_name: string; + committer_email: string; + committed_date: string; + web_url: string; + ci_reports: any[]; + stats: { + additions: number; + deletions: number; + total: number; + }; + status: string; + project_id: number; + last_pipeline: { + id: number; + project_id: number; + sha: string; + ref: string; + status: string; + created_at: string; + updated_at: string; + web_url: string; + }; +} + +export interface ProjectInfo { + id: number; + description: string; + main_language: string; + name: string; + name_with_namespace: string; + path: string; + path_with_namespace: string; + created_at: string; + default_branch: string; + tag_list: string[]; + ssh_url_to_repo: string; + http_url_to_repo: string; + web_url: string; + avatar_url: string | null; + forks_count: number; + star_count: number; + last_activity_at: string; + bu: string; + properties: { + node_version: string; + }; + packages_enabled: boolean; + empty_repo: boolean; + archived: boolean; + visibility: string; + resolve_outdated_diff_discussions: boolean; + container_registry_enabled: boolean; + issues_enabled: boolean; + merge_requests_enabled: boolean; + wiki_enabled: boolean; + jobs_enabled: boolean; + snippets_enabled: boolean; + service_desk_enabled: boolean; + service_desk_address: string | null; + can_create_merge_request_in: boolean; + issues_access_level: string; + repository_access_level: string; + merge_requests_access_level: string; + forking_access_level: string; + wiki_access_level: string; + builds_access_level: string; + snippets_access_level: string; + pages_access_level: string; + operations_access_level: string; + analytics_access_level: string; + emails_disabled: any; // You may want to define a more specific type here + shared_runners_enabled: boolean; + lfs_enabled: boolean; + creator_id: number; + import_status: string; + import_error: string | null; + open_issues_count: number; + runners_token: string; + ci_default_git_depth: number; + ci_forward_deployment_enabled: boolean; + public_jobs: boolean; + build_git_strategy: string; + build_timeout: number; + auto_cancel_pending_pipelines: string; + build_coverage_regex: string | null; + ci_config_path: string | null; + shared_with_groups: any[]; // You may want to define a more specific type here + only_allow_merge_if_pipeline_succeeds: boolean; + allow_merge_on_skipped_pipeline: any; // You may want to define a more specific type here + restrict_user_defined_variables: boolean; + request_access_enabled: boolean; + only_allow_merge_if_all_discussions_are_resolved: boolean; + remove_source_branch_after_merge: boolean; + printing_merge_request_link_enabled: boolean; + merge_method: string; + suggestion_commit_message: string | null; + auto_devops_enabled: boolean; + auto_devops_deploy_strategy: string; + autoclose_referenced_issues: boolean; +} + +const { GITLAB_URL } = process.env; + +export const getFileInfo = async ( + { + projectID, + filepath, + commitSha, + }: { projectID: string; filepath: string; commitSha: string }, + token: string, +) => { + return await axios + .get( + `${GITLAB_URL}/api/v4/projects/${projectID}/repository/files/${filepath}`, + { + params: { + ref: commitSha, + }, + headers: { + // Authorization: `Bearer ${token}`, + 'private-token': process.env.PRIVATE_TOKEN, + }, + }, + ) + .then(({ data }) => data); +}; + +export const getCommits = async ( + { projectID, commitShas }: { projectID: string; commitShas: string[] }, + token: string, +) => { + return await Promise.all( + commitShas.map((commitSha) => + axios + .get( + `${GITLAB_URL}/api/v4/projects/${projectID}/repository/commits/${commitSha}`, + { + headers: { + // Authorization: `Bearer ${token}`, + 'private-token': process.env.PRIVATE_TOKEN, + }, + }, + ) + .then(({ data }) => data), + ), + ); +}; + +export async function getProjectByID(projectID: string, token: string) { + return await axios + .get(`${GITLAB_URL}/api/v4/projects/${projectID}`, { + headers: { + // Authorization: `Bearer ${token}`, + 'private-token': process.env.PRIVATE_TOKEN, + }, + }) + .then(({ data }) => data); +} diff --git a/packages/canyon-backend/src/app.controller.ts b/packages/canyon-backend/src/app.controller.ts index 7f87f0c5..fae5f80b 100755 --- a/packages/canyon-backend/src/app.controller.ts +++ b/packages/canyon-backend/src/app.controller.ts @@ -1,4 +1,9 @@ import { Body, Controller, Get, Post, Request } from '@nestjs/common'; @Controller() -export class AppController {} +export class AppController { + @Get('vi/health') + viHealth() { + return '230614ms'; + } +} diff --git a/packages/canyon-backend/src/coverage/coverage.controller.ts b/packages/canyon-backend/src/coverage/coverage.controller.ts index 4bda3db1..e1633d33 100755 --- a/packages/canyon-backend/src/coverage/coverage.controller.ts +++ b/packages/canyon-backend/src/coverage/coverage.controller.ts @@ -30,22 +30,16 @@ export class CoverageController { @Body() coverageClientDto: CoverageClientDto, @Request() req: any, ): Promise { - console.log(req.user); return this.coverageClientService.invoke( - req?.user?.id || 1, + req.user.id, coverageClientDto, req.headers['user-agent'], req.ip, ); } - @Get('coverage/summary') - coverageSummary(): Promise { - return this.retrieveCoverageSummaryService.invoke(); - } - - @Get('vi/health') - viHealth() { - return '230614ms'; - } + // @Get('coverage/summary') + // coverageSummary(): Promise { + // // return this.retrieveCoverageSummaryService.invoke(); + // } } diff --git a/packages/canyon-backend/src/project/models/project-chart-data.model.ts b/packages/canyon-backend/src/project/models/project-chart-data.model.ts new file mode 100644 index 00000000..a7b4caaf --- /dev/null +++ b/packages/canyon-backend/src/project/models/project-chart-data.model.ts @@ -0,0 +1,17 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class ProjectChartDataModel { + @Field(() => Number, { + description: '整体覆盖率', + }) + statements: number; + @Field(() => Number, { + description: 'New Lines', + }) + newlines: number; + @Field(() => String, { + description: 'sha', + }) + sha: string; +} diff --git a/packages/canyon-backend/src/project/project-pages.model.ts b/packages/canyon-backend/src/project/models/project-pages.model.ts similarity index 82% rename from packages/canyon-backend/src/project/project-pages.model.ts rename to packages/canyon-backend/src/project/models/project-pages.model.ts index 1d0dd6a2..5ebd5c11 100644 --- a/packages/canyon-backend/src/project/project-pages.model.ts +++ b/packages/canyon-backend/src/project/models/project-pages.model.ts @@ -1,5 +1,5 @@ import {Field, ID, ObjectType, Resolver} from '@nestjs/graphql'; -import { Project } from './project.model'; +import { Project } from '../project.model'; @ObjectType() export class ProjectPagesModel { diff --git a/packages/canyon-backend/src/project/models/project-records.model.ts b/packages/canyon-backend/src/project/models/project-records.model.ts new file mode 100644 index 00000000..91e7ab15 --- /dev/null +++ b/packages/canyon-backend/src/project/models/project-records.model.ts @@ -0,0 +1,98 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +class Log { + @Field(() => String, { + description: 'ID', + }) + id: string; + @Field(() => String, { + description: 'Commit Sha', + }) + commitSha: string; + @Field(() => String, { + description: '上报ID', + }) + reportID: string; + @Field(() => String, { + description: '关系ID', + }) + relationID: string; + @Field(() => Date, { + description: '创建时间', + }) + createdAt: string; + @Field(() => String, { + description: '上报人', + }) + reporterUsername: string; + @Field(() => String, { + description: '上报人头像', + }) + reporterAvatar: string; + + @Field(() => Number, { + description: '新增', + }) + newlines: number; + + @Field(() => Number, { + description: '全量', + }) + statements: number; +} + +@ObjectType() +export class ProjectRecordsModel { + @Field(() => String, { + description: 'commit信息', + }) + message: string; + @Field(() => String, { + description: 'commit sha', + }) + commitSha: string; + + @Field(() => String, { + description: 'Compare Target', + }) + compareTarget: string; + + @Field(() => String, { + description: 'branch', + }) + branch: string; + + @Field(() => String, { + description: 'Compare Url', + }) + compareUrl: string; + + @Field(() => String, { + description: 'web url', + }) + webUrl: string; + + @Field(() => Number, { + description: '新增', + }) + newlines: number; + + @Field(() => Number, { + description: '全量', + }) + statements: number; + + @Field(() => Date, { + description: '最近一次上报', + }) + lastReportTime: string; + @Field(() => Number, { + description: '上报次数', + }) + times: number; + @Field(() => [Log], { + description: '上报日志', + }) + logs: Log[]; +} diff --git a/packages/canyon-backend/src/project/project.module.ts b/packages/canyon-backend/src/project/project.module.ts index eb0fb34e..eb006c61 100755 --- a/packages/canyon-backend/src/project/project.module.ts +++ b/packages/canyon-backend/src/project/project.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/prisma/prisma.module'; import { ProjectResolver } from './project.resolver'; -import { ProjectService } from './project.service'; +import { ProjectService } from './services/project.service'; +import {GetProjectChartDataService} from "./services/get-project-chart-data.service"; @Module({ imports: [PrismaModule], controllers: [], - providers: [ProjectResolver, ProjectService], + providers: [ProjectResolver, ProjectService,GetProjectChartDataService], exports: [], }) export class ProjectModule {} diff --git a/packages/canyon-backend/src/project/project.resolver.ts b/packages/canyon-backend/src/project/project.resolver.ts index fe503672..62031e97 100755 --- a/packages/canyon-backend/src/project/project.resolver.ts +++ b/packages/canyon-backend/src/project/project.resolver.ts @@ -4,24 +4,49 @@ import { UseGuards } from '@nestjs/common'; import { GqlUser } from '../decorators/gql-user.decorator'; import { AuthUser } from '../types/AuthUser'; import { Project } from './project.model'; -import { ProjectPagesModel } from './project-pages.model'; -import { ProjectService } from './project.service'; +import { ProjectPagesModel } from './models/project-pages.model'; +import { ProjectService } from './services/project.service'; +import { GetProjectChartDataService } from './services/get-project-chart-data.service'; +import { ProjectChartDataModel } from './models/project-chart-data.model'; +import {ProjectRecordsModel} from "./models/project-records.model"; +import {GetProjectRecordsService} from "./services/get-project-records.service"; @Resolver(() => 'Project') export class ProjectResolver { - constructor(private readonly projectService: ProjectService) {} + constructor( + private readonly projectService: ProjectService, + private readonly getProjectChartDataService: GetProjectChartDataService, + private readonly getProjectRecordsService: GetProjectRecordsService, + ) {} @Query(() => ProjectPagesModel, { description: '获取Project', }) - @UseGuards(GqlAuthGuard) getProjects( - @GqlUser() user: AuthUser, @Args('current', { type: () => Int }) current: number, @Args('pageSize', { type: () => Int }) pageSize: number, @Args('keyword', { type: () => String }) keyword: string, ): Promise { - console.log(current, pageSize); return this.projectService.getProjects(current, pageSize, keyword); - // console.log(da); - // return da } + + @Query(() => [ProjectChartDataModel], { + description: '获取Project图标', + }) + getProjectChartData( + @Args('projectID', { type: () => String }) projectID: string, + ): Promise { + return this.getProjectChartDataService.invoke(projectID); + } + + @Query(() => [ProjectRecordsModel], { + description: '获取Project记录', + }) + getProjectRecords( + @Args('projectID', { type: () => String }) projectID: string, + ): Promise { + return this.getProjectRecordsService.invoke(projectID); + } + + // getProjectRecords + // getProjectChartData + // getProjectCompartmentData } diff --git a/packages/canyon-backend/src/project/services/get-project-chart-data.service.ts b/packages/canyon-backend/src/project/services/get-project-chart-data.service.ts new file mode 100644 index 00000000..f62ae365 --- /dev/null +++ b/packages/canyon-backend/src/project/services/get-project-chart-data.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../prisma/prisma.service'; +import { calculateCoverageOverviewByConditionFilter } from '../../utils/summary'; +// import { getProjectByID } from '../adapter/gitlab.adapter'; +@Injectable() +export class GetProjectChartDataService { + constructor(private readonly prisma: PrismaService) {} + async invoke(projectID) { + const allCovTypeCoverages = await this.prisma.coverage.findMany({ + where: { + projectID: projectID, + covType: 'all', + }, + orderBy: { + createdAt: 'desc', + }, + }); + + const summarys = await this.prisma.summary.findMany({ + where: { + sha: { + in: [...allCovTypeCoverages.map((item) => item.sha)], + }, + }, + }); + + return allCovTypeCoverages + .map((item) => { + return { + sha: item.sha, + statements: calculateCoverageOverviewByConditionFilter( + summarys.filter( + ({ sha: curCommitSha, covType }) => + curCommitSha === item.sha && 'all' === covType, + ), + ).statements.pct, + newlines: calculateCoverageOverviewByConditionFilter( + summarys.filter( + ({ sha: curCommitSha, covType }) => + curCommitSha === item.sha && 'all' === covType, + ), + ).newlines.pct, + }; + }) + .reverse() + .slice(-10); + } +} diff --git a/packages/canyon-backend/src/project/services/get-project-compartment-data.service.ts b/packages/canyon-backend/src/project/services/get-project-compartment-data.service.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/canyon-backend/src/project/services/get-project-records.service.ts b/packages/canyon-backend/src/project/services/get-project-records.service.ts new file mode 100644 index 00000000..de3bf9cb --- /dev/null +++ b/packages/canyon-backend/src/project/services/get-project-records.service.ts @@ -0,0 +1,118 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../prisma/prisma.service'; +import { calculateCoverageOverviewByConditionFilter } from '../../utils/summary'; +import process from 'process'; +import { getCommits } from '../../adapter/gitlab.adapter'; +// import { getProjectByID } from '../adapter/gitlab.adapter'; +@Injectable() +export class GetProjectRecordsService { + constructor(private readonly prisma: PrismaService) {} + async invoke(projectID) { + const allCovTypeCoverages = await this.prisma.coverage.findMany({ + where: { + projectID: projectID, + covType: 'all', + }, + orderBy: { + createdAt: 'desc', + }, + }); + const project = await this.prisma.project.findFirst({ + where: { + id: projectID, + }, + }); + const summarys = await this.prisma.summary.findMany({ + where: { + sha: { + in: [...allCovTypeCoverages.map((item) => item.sha)], + }, + }, + }); + + const aggCovTypeCoverages = await this.prisma.coverage.findMany({ + where: { + projectID: projectID, + covType: 'agg', + }, + orderBy: { + createdAt: 'desc', + }, + }); + const commits = await getCommits( + { + projectID, + commitShas: allCovTypeCoverages.map((item) => item.sha), + }, + 'accessToken', + ); + const users = await this.prisma.user.findMany({ + select: { + nickname: true, + username: true, + avatar: true, + id: true, + }, + }); + + const aggregatedReports: any = aggCovTypeCoverages.reduce((acc, report) => { + const sha = report.sha; + const log = { + ...report, + reporterUsername: users.find(({ id: uId }) => uId === report.reporter) + ?.nickname, + reporterAvatar: users.find(({ id: uId }) => uId === report.reporter) + ?.avatar, + newlines: calculateCoverageOverviewByConditionFilter( + summarys.filter( + (item) => + item.sha === sha && + item.reportID === report.reportID && + 'agg' === item.covType, + ), + ).newlines.pct, + statements: calculateCoverageOverviewByConditionFilter( + summarys.filter( + (item) => + item.sha === sha && + item.reportID === report.reportID && + 'agg' === item.covType, + ), + ).statements.pct, + }; + + if (!acc[sha]) { + acc[sha] = { + statements: calculateCoverageOverviewByConditionFilter( + summarys.filter( + (item) => item.sha === sha && 'all' === item.covType, + ), + ).statements.pct, + newlines: calculateCoverageOverviewByConditionFilter( + summarys.filter( + (item) => item.sha === sha && 'all' === item.covType, + ), + ).newlines.pct, + sha, + compareTarget: report.compareTarget, + compareUrl: `${process.env.GITLAB_URL}/${project.pathWithNamespace}/-/compare/${report.compareTarget}...${report.sha}`, + webUrl: commits.find(({ id }) => id === sha)?.web_url || '???', + message: commits.find(({ id }) => id === sha)?.message || '???', + branch: report.branch, + lastReportTime: aggCovTypeCoverages.filter( + ({ sha: curCommitSha }) => curCommitSha === sha, + )[0].createdAt, + times: 1, + logs: [log], + }; + } else { + acc[sha].logs.push(log); + acc[sha].times += 1; + } + + return acc; + }, {}); + + return Object.values(aggregatedReports); + } +} diff --git a/packages/canyon-backend/src/project/project.service.ts b/packages/canyon-backend/src/project/services/project.service.ts similarity index 97% rename from packages/canyon-backend/src/project/project.service.ts rename to packages/canyon-backend/src/project/services/project.service.ts index b827dedd..d5b38408 100755 --- a/packages/canyon-backend/src/project/project.service.ts +++ b/packages/canyon-backend/src/project/services/project.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service'; +import { PrismaService } from '../../prisma/prisma.service'; // import { getProjectByID } from '../adapter/gitlab.adapter'; function parseGitLabUrl(gitLabUrl) { // 匹配 GitLab URL 的正则表达式 diff --git a/packages/canyon-backend/src/utils/summary.ts b/packages/canyon-backend/src/utils/summary.ts new file mode 100755 index 00000000..daa8b211 --- /dev/null +++ b/packages/canyon-backend/src/utils/summary.ts @@ -0,0 +1,31 @@ +import { percent } from './utils'; +import { CoverageSummaryData, Totals } from 'istanbul-lib-coverage'; + +export function calculateCoverageOverviewByConditionFilter( + data, +): CoverageSummaryData & { newlines: Totals } { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return ['newlines', 'lines', 'statements', 'branches', 'functions'].reduce( + (previousValue, currentValue) => { + const ret = { + total: data + .filter(({ metricType }) => metricType === currentValue) + .reduce((acc, item) => acc + item.total, 0), + covered: data + .filter(({ metricType }) => metricType === currentValue) + .reduce((acc, item) => acc + item.covered, 0), + skipped: data + .filter(({ metricType }) => metricType === currentValue) + .reduce((acc, item) => acc + item.skipped, 0), + pct: 0, + }; + ret.pct = percent(ret.covered, ret.total); + return { + ...previousValue, + [currentValue]: ret, + }; + }, + {}, + ); +}