Skip to content

Commit

Permalink
feat: 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 committed Sep 17, 2020
1 parent be06135 commit 062a6eb
Show file tree
Hide file tree
Showing 46 changed files with 686 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ApiBearerAuth, ApiOkResponse, ApiQuery, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { AuthGuard, Credentials, RequestCredentials } from '@pimp-my-pr/server/auth/public';
import {
GetPrTimelineQuery,
TimelineFacade
} from '@pimp-my-pr/server/repository/core/application-services';
import { PrTimelineReadModel } from '@pimp-my-pr/server/repository/core/application-services';
import { TimelineStepType } from '@pimp-my-pr/server/repository/core/domain';

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

@ApiQuery({ name: 'step', enum: ['day', 'week', 'month', 'quarter', 'year'] })
@ApiQuery({ name: 'dateTimestamp', type: Number })
@ApiQuery({ name: 'queryEndTimestamp', type: Number })
@ApiQuery({ name: 'count', type: Number })
@ApiOkResponse({ type: [PrTimelineReadModel] })
@Get('pr/:repositoryId')
listRepositories(
@Credentials() credentials: RequestCredentials,
@Param('repositoryId') repositoryId: string,
@Query('step') step: TimelineStepType,
@Query('dateTimestamp') dateTimestamp: number,
@Query('queryEndTimestamp') queryEndTimeStamp: number,
@Query('count') count: number
): Promise<PrTimelineReadModel> {
return this.timelineFacade.getPrTimeLine(
new GetPrTimelineQuery(
step,
Number(dateTimestamp),
Number(count),
credentials.token,
repositoryId,
credentials.platform,
Number(queryEndTimeStamp)
)
);
}
}
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,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GetPrTimelineHandler } from './get-pr-timeline.handler';

describe('GetPrTimelineHandler', () => {
let handler: GetPrTimelineHandler;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GetPrTimelineHandler]
}).compile();

handler = module.get<GetPrTimelineHandler>(GetPrTimelineHandler);
});

test('creates itself', () => {
expect(handler).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { QueryHandler, IQueryHandler } 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,
PrEntity,
traversePagesUntil
} 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';

@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, dateTimestamp, count, token, platform, queryEndTimeStamp } = 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, 'all', page, 100),
100,
new Date(queryEndTimeStamp)
);
const records = getTimeLineHistory(prs, step, new Date(dateTimestamp), count);
return prTimelineModelFactory(records, query);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IQuery } from '@nestjs/cqrs';
import { TimelineStepType } from '@pimp-my-pr/server/repository/core/domain';
import { Platform } from '@pimp-my-pr/shared/domain';

export class GetPrTimelineQuery implements IQuery {
constructor(
public step: TimelineStepType,
public dateTimestamp: number,
public count: number,
public token: string,
public repositoryId: string,
public platform: Platform,
public queryEndTimeStamp: number
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TimelineRecord, TimelineStepType } from '@pimp-my-pr/server/repository/core/domain';
import { ApiProperty } from '@nestjs/swagger';

export class PrTimelineReadModel {
@ApiProperty({
enum: ['day', 'week', 'month', 'quarter', 'year']
})
step: TimelineStepType;

@ApiProperty()
dateFrom: Date;

@ApiProperty({
type: [TimelineRecord]
})
data: TimelineRecord[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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
): PrTimelineReadModel => {
return {
data: prRecords,
step: query.step,
dateFrom: new Date(Number(query.dateTimestamp))
};
};
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,14 @@
import { PrEntity } from '@pimp-my-pr/server/repository/core/domain';
import { PrStateType } 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,
prState?: PrStateType,
page?: number,
onPage?: number
): Promise<PrEntity[]>;
}
5 changes: 5 additions & 0 deletions libs/server/repository/core/domain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export * from './lib/entities/repository.entity';
export * from './lib/entities/reviewer.entity';
export * from './lib/exceptions/repository-not-found.exception';
export * from './lib/utils/repository-name-extract.util';
export * from './lib/utils/timeline/get-timeline-history';
export * from './lib/utils/traverse-pages-until.util';
export * from './lib/entities/timeline-record.entity';
export * from './lib/types/pr-state.type';
export * from './lib/types/timeline-step.type';
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,12 @@
import { ApiProperty } from '@nestjs/swagger';

export class TimelineRecord {
@ApiProperty()
label: number;
@ApiProperty()
sumCount: number;
@ApiProperty()
avgCount: number;
@ApiProperty()
avgWaitingTime: number;
}
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,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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TimelineDateRange } from './timeline-date-range.interface';

export interface TimelineDivisionBase {
[timestamp: number]: TimelineDateRange;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type PrStateType = 'open' | 'closed' | 'all';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type TimelineStepType = 'day' | 'month' | 'week' | 'quarter' | 'year';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function iteratorToArray<T>(a: Iterator<T>): T[] {
const result: T[] = [];
let current = a.next();
while (!current.done) {
result.push(current.value);
current = a.next();
}
return result;
}

export async function asyncIteratorToArray<T>(a: AsyncIterator<T>): Promise<T[]> {
const result: T[] = [];
let current = await a.next();
while (!current.done) {
result.push(current.value);
current = await a.next();
}
return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { padNumberToPlaces } from './pad-number-to-places.util';

describe('#padNumberToPlaces', () => {
it('should does not change input if it is longer than specified', () => {
expect(padNumberToPlaces(1000, 3)).toBe('1000');
});

it('should add zeros at he beginning if the number is shorter', () => {
expect(padNumberToPlaces(56, 4)).toBe('0056');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const padNumberToPlaces = (num: number, places = 2): string =>
String(num).padStart(places, '0');
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { decreaseDateByTimelineStep } from './decrease-date-by-timeline-step.util';

describe('#decreaseDateByTimelineStep', () => {
it('should subtract n days', () => {
expect(decreaseDateByTimelineStep(new Date('1995-12-17T00:00:00'), 'day', 7)).toEqual(
new Date('1995-12-10T00:00:00')
);
});
it('should subtract n weeks', () => {
expect(decreaseDateByTimelineStep(new Date('1995-12-17T00:00:00'), 'week', 2)).toEqual(
new Date('1995-12-03T00:00:00')
);
});
it('should subtract n months', () => {
expect(decreaseDateByTimelineStep(new Date('1995-12-17T00:00:00'), 'month', 7)).toEqual(
new Date('1995-05-01T00:00:00')
);
});
it('should subtract n quarters', () => {
expect(decreaseDateByTimelineStep(new Date('1995-12-17T00:00:00'), 'quarter', 2)).toEqual(
new Date('1995-06-01T00:00:00')
);
});
it('should subtract n days', () => {
expect(decreaseDateByTimelineStep(new Date('2000-12-17T00:00:00'), 'year', 5)).toEqual(
new Date('1995-12-01T00:00:00')
);
});
it('should return new object', () => {
const date = new Date('1995-12-17T00:00:00');
expect(decreaseDateByTimelineStep(date, 'day', 5)).not.toBe(date);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TimelineStepType } from '../../types/timeline-step.type';
import { subtractDaysFromDate } from './subtract-days-from-date.util';
import { subtractMonthsFromDate } from './subtract-months-from-date.util';

export const decreaseDateByTimelineStep = (
date: Date,
step: TimelineStepType,
numSteps: number
): Date => {
switch (step) {
case 'day':
return subtractDaysFromDate(date, numSteps);
case 'week':
return subtractDaysFromDate(date, 7 * numSteps);
case 'month':
return subtractMonthsFromDate(date, numSteps);
case 'quarter':
return subtractMonthsFromDate(date, 3 * numSteps);
case 'year':
return subtractMonthsFromDate(date, 12 * numSteps);
}
};
Loading

0 comments on commit 062a6eb

Please sign in to comment.