Skip to content

Commit

Permalink
feat(api-repository): add backend for timeline statistics
Browse files Browse the repository at this point in the history
-add endpoint for acquiring statistics
-upgrade github/gitlab/bitbucket remote repositories
to handle pagination and state filters
-add util for processing prs into timeline staticstics,
currently the following fields are available:
{
    sumCount: all PR that were open in subperiod
    avgCount: avg count of PR that were open in subperiod
    avgWaitingTime: avg time taht prs were waiting since creation
}

resolve valueadd-poland#160 - backend
  • Loading branch information
maciejBart99 authored and Maciej Łukasik committed Oct 5, 2020
1 parent be06135 commit 06ffa08
Show file tree
Hide file tree
Showing 63 changed files with 872 additions and 23 deletions.
32 changes: 32 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,38 @@
"style": "scss"
}
}
},
"server-repository-util": {
"projectType": "library",
"root": "libs/server/repository/util",
"sourceRoot": "libs/server/repository/util/src",
"prefix": "pimp-my-pr",
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"libs/server/repository/util/tsconfig.lib.json",
"libs/server/repository/util/tsconfig.spec.json"
],
"exclude": ["**/node_modules/**", "!libs/server/repository/util/**"]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/server/repository/util/jest.config.js",
"tsConfig": "libs/server/repository/util/tsconfig.spec.json",
"passWithNoTests": true,
"setupFile": "libs/server/repository/util/src/test-setup.ts"
}
}
},
"schematics": {
"@nrwl/angular:component": {
"style": "scss"
}
}
}
},
"cli": {
Expand Down
4 changes: 3 additions & 1 deletion apps/pmp-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ async function bootstrap(): Promise<void> {
const globalPrefix = 'api';

app.setGlobalPrefix(globalPrefix);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalPipes(
new ValidationPipe({ transform: true, transformOptions: { enableImplicitConversion: true } })
);

const port = process.env.port || 3333;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ApiBearerAuth, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Param, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
import { AuthGuard, Credentials, RequestCredentials } from '@pimp-my-pr/server/auth/public';
import {
GetPrTimelineQuery,
PrTimelineReadModel,
TimelineFacade
} from '@pimp-my-pr/server/repository/core/application-services';
import { GenerateTimelineDto } from '../dtos/generate-timeline.dto';
import { decreaseDateByTimelineStep } from '@pimp-my-pr/server/repository/util';
import { TimelineStep } from '@pimp-my-pr/shared/domain';

@ApiTags('timeline')
@UseGuards(AuthGuard)
@ApiBearerAuth()
@Controller('timeline')
export class TimelineController {
constructor(private timelineFacade: TimelineFacade) {}

@ApiOkResponse({ type: [PrTimelineReadModel] })
@Get('pr/:repositoryId')
generateTimeline(
@Credentials() credentials: RequestCredentials,
@Param('repositoryId') repositoryId: string,
@Query() query: GenerateTimelineDto
): Promise<PrTimelineReadModel> {
return this.timelineFacade.getPrTimeLine(
new GetPrTimelineQuery(
query.step,
query.timelineFrom,
query.timelineTo,
credentials.token,
repositoryId,
credentials.platform,
// TODO: Find a better solution to handle this problem
decreaseDateByTimelineStep(query.timelineFrom, TimelineStep.MONTH, 3)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDate, IsIn } from 'class-validator';
import { Transform } from 'class-transformer';
import { TimelineStep } from '@pimp-my-pr/shared/domain';

export class GenerateTimelineDto {
@ApiProperty({ enum: TimelineStep })
@IsIn(Object.values(TimelineStep))
step: TimelineStep;

@ApiProperty({
description: 'Beginning of the tracked period - every subsequent record is older'
})
@IsDate()
@Transform(value => new Date(value))
timelineFrom: Date;

@ApiProperty({
description: 'Beginning of the tracked period - every subsequent record is older'
})
@IsDate()
@Transform(value => new Date(value))
timelineTo: Date;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { ServerRepositoryShellModule } from '@pimp-my-pr/server/repository/shell
import { RepositoryController } from './controllers/repository.controller';
import { StatisticsController } from './controllers/statistics.controller';
import { UserRepositoryGuard } from './guards/user-repository.guard';
import { TimelineController } from './controllers/timeline.controller';

@Module({
imports: [ServerRepositoryShellModule],
controllers: [RepositoryController, StatisticsController],
controllers: [RepositoryController, StatisticsController, TimelineController],
providers: [UserRepositoryGuard]
})
export class ServerRepositoryApiRestModule {}
3 changes: 3 additions & 0 deletions libs/server/repository/core/application-services/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ export * from './lib/queries/list-repositories/list-repositories.read-model';
export * from './lib/commands/edit-repository/edit-repository.command';
export * from './lib/queries/get-single-repository-data/get-single-repository-data.handler';
export * from './lib/queries/get-single-repository-data/single-repository-data.read-model';
export * from './lib/queries/get-pr-timeline/get-pr-timeline.query';
export * from './lib/queries/get-pr-timeline/pr-timeline.read-model';
export * from './lib/timeline.facade';
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetPrTimelineQuery } from './get-pr-timeline.query';
import {
PrRepository,
prRepositoryFactoryToken,
RepositoryRepository
} from '@pimp-my-pr/server/repository/core/domain-services';
import { PrTimelineReadModel } from '@pimp-my-pr/server/repository/core/application-services';
import {
getTimeLineHistory,
InvalidTimelineParametersException,
PrEntity,
PrState
} from '@pimp-my-pr/server/repository/core/domain';
import { prTimelineModelFactory } from '../../read-models/factories/pr-timeline-model.factory';
import { Inject } from '@nestjs/common';
import { Platform } from '@pimp-my-pr/shared/domain';
import { getStepsCount, traversePagesUntil } from '@pimp-my-pr/server/repository/util';

@QueryHandler(GetPrTimelineQuery)
export class GetPrTimelineHandler implements IQueryHandler<GetPrTimelineQuery> {
constructor(
@Inject(prRepositoryFactoryToken)
private prRepositoryFactory: (platform: Platform) => PrRepository,
private repoRepository: RepositoryRepository
) {}

async execute(query: GetPrTimelineQuery): Promise<PrTimelineReadModel> {
const { repositoryId, step, timelineFrom, timelineTo, token, platform, createdAfter } = query;
const { fullName } = await this.repoRepository.getById(repositoryId);
const prRepository = this.prRepositoryFactory(platform);

const prs = await traversePagesUntil<PrEntity>(
async page =>
await prRepository.findByRepositoryId(fullName, token, {
prState: PrState.ALL,
page,
onPage: 100
}),
100,
createdAfter
);
const stepsCount = getStepsCount(timelineFrom, timelineTo, step);
if (stepsCount === 0) throw new InvalidTimelineParametersException('timelineFrom');

const records = getTimeLineHistory(prs, step, timelineTo, stepsCount);
return prTimelineModelFactory(records, query, prs.length);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IQuery } from '@nestjs/cqrs';
import { Platform, TimelineStep } from '@pimp-my-pr/shared/domain';

export class GetPrTimelineQuery implements IQuery {
constructor(
public step: TimelineStep,
public timelineFrom: Date,
public timelineTo: Date,
public token: string,
public repositoryId: string,
public platform: Platform,
public createdAfter: Date
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TimelineRecord } from '@pimp-my-pr/server/repository/core/domain';
import { ApiProperty } from '@nestjs/swagger';
import { TimelineStep } from '@pimp-my-pr/shared/domain';

export class PrTimelineReadModel {
@ApiProperty({
enum: TimelineStep
})
step: TimelineStep;

@ApiProperty()
dateFrom: Date;

@ApiProperty()
dateTo: Date;

@ApiProperty({
description: 'Pull requests were taken into account that were created after this date'
})
createdAfter: Date;

@ApiProperty({
type: [TimelineRecord]
})
data: TimelineRecord[];

@ApiProperty()
totalPrs: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
GetPrTimelineQuery,
PrTimelineReadModel
} from '@pimp-my-pr/server/repository/core/application-services';
import { TimelineRecord } from '@pimp-my-pr/server/repository/core/domain';

export const prTimelineModelFactory = (
prRecords: TimelineRecord[],
query: GetPrTimelineQuery,
totalPrs: number
): PrTimelineReadModel => {
return {
data: prRecords,
step: query.step,
dateFrom: query.timelineFrom,
dateTo: query.timelineTo,
createdAfter: query.createdAfter,
totalPrs
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { RepositoryFacade } from './repository.facade';
import { DeleteRepositoryHandler } from './commands/delete-repository/delete-repository.handler';
import { EditRepositoryHandler } from './commands/edit-repository/edit-repository.handler';
import { GetSingleRepositoryDataHandler } from './queries/get-single-repository-data/get-single-repository-data.handler';
import { GetPrTimelineHandler } from './queries/get-pr-timeline/get-pr-timeline.handler';
import { TimelineFacade } from './timeline.facade';

const QueryHandlers = [
AddRepositoryHandler,
Expand All @@ -20,12 +22,13 @@ const QueryHandlers = [
GetSingleRepositoryDataHandler,
ListRepositoriesStatisticsHandler,
ListReviewersStatisticsHandler,
ListRepositoriesHandler
ListRepositoriesHandler,
GetPrTimelineHandler
];

@Module({
imports: [CqrsModule],
providers: [RepositoryFacade, ...QueryHandlers],
exports: [RepositoryFacade]
providers: [RepositoryFacade, TimelineFacade, ...QueryHandlers],
exports: [RepositoryFacade, TimelineFacade]
})
export class ServerRepositoryCoreApplicationServicesModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { QueryBus } from '@nestjs/cqrs';
import { GetPrTimelineQuery } from './queries/get-pr-timeline/get-pr-timeline.query';
import { PrTimelineReadModel } from './queries/get-pr-timeline/pr-timeline.read-model';
import { Injectable } from '@nestjs/common';

@Injectable()
export class TimelineFacade {
constructor(private queryBus: QueryBus) {}

getPrTimeLine(query: GetPrTimelineQuery): Promise<PrTimelineReadModel> {
return this.queryBus.execute(query);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { PrEntity } from '@pimp-my-pr/server/repository/core/domain';
import { PrRepositoryFetchParams } from '@pimp-my-pr/server/repository/core/domain';

export const prRepositoryFactoryToken = Symbol('prRepositoryFactory');

export abstract class PrRepository {
abstract findByRepositoryId(repositoryId: string, token: string): Promise<PrEntity[]>;
abstract findByRepositoryId(
repositoryId: string,
token: string,
params?: PrRepositoryFetchParams
): Promise<PrEntity[]>;
}
10 changes: 10 additions & 0 deletions libs/server/repository/core/domain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,14 @@ export * from './lib/entities/pr.entity';
export * from './lib/entities/repository.entity';
export * from './lib/entities/reviewer.entity';
export * from './lib/exceptions/repository-not-found.exception';
export * from './lib/exceptions/invalid-timeline-parameters.exception';
export * from './lib/interfaces/i-time-trackable.interface';
export * from './lib/interfaces/timeline-bucket-item.interface';
export * from './lib/interfaces/timeline-bucket.interface';
export * from './lib/interfaces/timeline-date-range.interface';
export * from './lib/interfaces/timeline-division-base.interface';
export * from './lib/interfaces/pr-repository-fetch-params.interface';
export * from './lib/utils/repository-name-extract.util';
export * from './lib/entities/timeline-record.entity';
export * from './lib/enums/pr-state.enum';
export * from './lib/utils/get-timeline-history';
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { f } from '@marcj/marshal';
import { AuthorEntity } from './author.entity';
import { ReviewerEntity } from './reviewer.entity';
import { ITimeTrackable } from '../interfaces/i-time-trackable.interface';

export class PrEntity {
export class PrEntity implements ITimeTrackable {
@f
additions: number;
@f
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';

export class TimelineRecord {
@ApiProperty()
dataFrom: Date;
@ApiProperty()
sumCount: number;
@ApiProperty()
avgCount: number;
@ApiProperty()
avgWaitingTime: number;
/**
* This property is required to calculate total prs in any period on timeline
*/
@ApiProperty()
closedBefore: number;
/**
* This property is required to calculate total prs in any period on timeline
*/
@ApiProperty()
openedAfter: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum PrState {
OPEN = 'OPEN',
CLOSED = 'CLOSED',
ALL = 'ALL'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CoreUnprocessableEntityException } from '@pimp-my-pr/server/shared/domain';

export class InvalidTimelineParametersException extends CoreUnprocessableEntityException {
constructor(paramName: string) {
super(`Invalid ${paramName}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ITimeTrackable {
createdAt: Date;
closedAt?: Date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PrState } from '../enums/pr-state.enum';

export interface PrRepositoryFetchParams {
prState?: PrState;
page?: number;
onPage?: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface TimelineBucketItem<T> {
entity: T;
timeIn: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TimelineBucketItem } from './timeline-bucket-item.interface';

export interface TimelineBuckets<T> {
[label: number]: TimelineBucketItem<T>[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface TimelineDateRange {
dateFrom: Date;
dateTo: Date;
}
Loading

0 comments on commit 06ffa08

Please sign in to comment.