diff --git a/libs/server/repository/api-rest/src/lib/controllers/timeline.controller.ts b/libs/server/repository/api-rest/src/lib/controllers/timeline.controller.ts new file mode 100644 index 00000000..2a7eedfc --- /dev/null +++ b/libs/server/repository/api-rest/src/lib/controllers/timeline.controller.ts @@ -0,0 +1,42 @@ +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, + TimelineFacade +} from '@pimp-my-pr/server/repository/core/application-services'; +import { PrTimelineReadModel } from '@pimp-my-pr/server/repository/core/application-services'; +import { GenerateTimelineDto } from '../dtos/generate-timeline.dto'; +import { decreaseDateByTimelineStep } from '@pimp-my-pr/server/repository/core/domain'; + +@ApiTags('timeline') +@UseGuards(AuthGuard) +@ApiBearerAuth() +@Controller('timeline') +export class TimelineController { + constructor(private timelineFacade: TimelineFacade) {} + + @ApiOkResponse({ type: [PrTimelineReadModel] }) + @UsePipes( + new ValidationPipe({ transform: true, transformOptions: { enableImplicitConversion: true } }) + ) + @Get('pr/:repositoryId') + generateTimeline( + @Credentials() credentials: RequestCredentials, + @Param('repositoryId') repositoryId: string, + @Query() query: GenerateTimelineDto + ): Promise { + 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, 'month', 3) + ) + ); + } +} diff --git a/libs/server/repository/api-rest/src/lib/dtos/generate-timeline.dto.ts b/libs/server/repository/api-rest/src/lib/dtos/generate-timeline.dto.ts new file mode 100644 index 00000000..f251d423 --- /dev/null +++ b/libs/server/repository/api-rest/src/lib/dtos/generate-timeline.dto.ts @@ -0,0 +1,26 @@ +import { TimelineStepType } from '@pimp-my-pr/server/repository/core/domain'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsDate, IsIn } from 'class-validator'; +import { Transform } from 'class-transformer'; + +const STEPS = ['day', 'week', 'month', 'quarter', 'year']; + +export class GenerateTimelineDto { + @ApiProperty({ enum: STEPS }) + @IsIn(STEPS) + step: TimelineStepType; + + @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; +} diff --git a/libs/server/repository/api-rest/src/lib/server-repository-api-rest.module.ts b/libs/server/repository/api-rest/src/lib/server-repository-api-rest.module.ts index ed645482..2077fd03 100644 --- a/libs/server/repository/api-rest/src/lib/server-repository-api-rest.module.ts +++ b/libs/server/repository/api-rest/src/lib/server-repository-api-rest.module.ts @@ -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 {} diff --git a/libs/server/repository/core/application-services/src/index.ts b/libs/server/repository/core/application-services/src/index.ts index e5803662..daae11b2 100644 --- a/libs/server/repository/core/application-services/src/index.ts +++ b/libs/server/repository/core/application-services/src/index.ts @@ -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'; diff --git a/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/get-pr-timeline.handler.ts b/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/get-pr-timeline.handler.ts new file mode 100644 index 00000000..26bf9f03 --- /dev/null +++ b/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/get-pr-timeline.handler.ts @@ -0,0 +1,55 @@ +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 { + getStepsCount, + getTimeLineHistory, + InvalidTimelineParametersException, + 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 { + constructor( + @Inject(prRepositoryFactoryToken) + private prRepositoryFactory: (platform: Platform) => PrRepository, + private repoRepository: RepositoryRepository + ) {} + + async execute(query: GetPrTimelineQuery): Promise { + 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( + async page => await prRepository.findByRepositoryId(fullName, token, 'all', page, 100), + 100, + createdAfter + ); + const prsSum = prs.reduce( + (total, current) => + (!current.closedAt || current.closedAt.getTime() > timelineFrom.getTime()) && + current.createdAt.getTime() < timelineTo.getTime() + ? total + 1 + : total, + 0 + ); + const stepsCount = getStepsCount(timelineFrom, timelineTo, step); + if (stepsCount === 0) throw new InvalidTimelineParametersException('timelineFrom'); + + const records = getTimeLineHistory(prs, step, timelineTo, stepsCount); + const avgCount = records.reduce((total, current) => total + current.avgCount, 0) / prsSum; + const avgWaitingTime = + records.reduce((total, current) => total + current.avgWaitingTime, 0) / prsSum; + return prTimelineModelFactory(records, query, prsSum, avgCount, avgWaitingTime); + } +} diff --git a/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/get-pr-timeline.query.ts b/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/get-pr-timeline.query.ts new file mode 100644 index 00000000..9e3c0a2b --- /dev/null +++ b/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/get-pr-timeline.query.ts @@ -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 timelineFrom: Date, + public timelineTo: Date, + public token: string, + public repositoryId: string, + public platform: Platform, + public createdAfter: Date + ) {} +} diff --git a/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/pr-timeline.read-model.ts b/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/pr-timeline.read-model.ts new file mode 100644 index 00000000..ca7d7405 --- /dev/null +++ b/libs/server/repository/core/application-services/src/lib/queries/get-pr-timeline/pr-timeline.read-model.ts @@ -0,0 +1,34 @@ +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() + 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; + + @ApiProperty() + avgCount: number; + + @ApiProperty() + avgWaitingTime: number; +} diff --git a/libs/server/repository/core/application-services/src/lib/read-models/factories/pr-timeline-model.factory.ts b/libs/server/repository/core/application-services/src/lib/read-models/factories/pr-timeline-model.factory.ts new file mode 100644 index 00000000..509437ea --- /dev/null +++ b/libs/server/repository/core/application-services/src/lib/read-models/factories/pr-timeline-model.factory.ts @@ -0,0 +1,24 @@ +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, + sum: number, + avgCount: number, + avgWaitingTime: number +): PrTimelineReadModel => { + return { + data: prRecords, + step: query.step, + dateFrom: query.timelineFrom, + dateTo: query.timelineTo, + createdAfter: query.createdAfter, + totalPrs: sum, + avgCount: avgCount, + avgWaitingTime: avgWaitingTime + }; +}; diff --git a/libs/server/repository/core/application-services/src/lib/server-repository-core-application-services.module.ts b/libs/server/repository/core/application-services/src/lib/server-repository-core-application-services.module.ts index 29be5f11..f81e9a19 100644 --- a/libs/server/repository/core/application-services/src/lib/server-repository-core-application-services.module.ts +++ b/libs/server/repository/core/application-services/src/lib/server-repository-core-application-services.module.ts @@ -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, @@ -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 {} diff --git a/libs/server/repository/core/application-services/src/lib/timeline.facade.ts b/libs/server/repository/core/application-services/src/lib/timeline.facade.ts new file mode 100644 index 00000000..0492cf32 --- /dev/null +++ b/libs/server/repository/core/application-services/src/lib/timeline.facade.ts @@ -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 { + return this.queryBus.execute(query); + } +} diff --git a/libs/server/repository/core/domain-services/src/lib/repositories/pr.repository.ts b/libs/server/repository/core/domain-services/src/lib/repositories/pr.repository.ts index ef9c8013..d5db40dd 100644 --- a/libs/server/repository/core/domain-services/src/lib/repositories/pr.repository.ts +++ b/libs/server/repository/core/domain-services/src/lib/repositories/pr.repository.ts @@ -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; + abstract findByRepositoryId( + repositoryId: string, + token: string, + prState?: PrStateType, + page?: number, + onPage?: number + ): Promise; } diff --git a/libs/server/repository/core/domain/src/index.ts b/libs/server/repository/core/domain/src/index.ts index e4e74253..2709ab3e 100644 --- a/libs/server/repository/core/domain/src/index.ts +++ b/libs/server/repository/core/domain/src/index.ts @@ -3,4 +3,12 @@ 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/utils/repository-name-extract.util'; +export * from './lib/utils/timeline/get-timeline-history'; +export * from './lib/utils/traverse-pages-until.util'; +export * from './lib/utils/timeline/get-steps-count.util'; +export * from './lib/utils/timeline/decrease-date-by-timeline-step.util'; +export * from './lib/entities/timeline-record.entity'; +export * from './lib/types/pr-state.type'; +export * from './lib/types/timeline-step.type'; diff --git a/libs/server/repository/core/domain/src/lib/entities/pr.entity.ts b/libs/server/repository/core/domain/src/lib/entities/pr.entity.ts index 1e5792fe..91ecccd8 100644 --- a/libs/server/repository/core/domain/src/lib/entities/pr.entity.ts +++ b/libs/server/repository/core/domain/src/lib/entities/pr.entity.ts @@ -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 diff --git a/libs/server/repository/core/domain/src/lib/entities/timeline-record.entity.ts b/libs/server/repository/core/domain/src/lib/entities/timeline-record.entity.ts new file mode 100644 index 00000000..9359e197 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/entities/timeline-record.entity.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TimelineRecord { + @ApiProperty() + dataFrom: Date; + @ApiProperty() + sumCount: number; + @ApiProperty() + avgCount: number; + @ApiProperty() + avgWaitingTime: number; +} diff --git a/libs/server/repository/core/domain/src/lib/exceptions/invalid-timeline-parameters.exception.ts b/libs/server/repository/core/domain/src/lib/exceptions/invalid-timeline-parameters.exception.ts new file mode 100644 index 00000000..3a984b88 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/exceptions/invalid-timeline-parameters.exception.ts @@ -0,0 +1,7 @@ +import { CoreUnprocessableEntityException } from '@pimp-my-pr/server/shared/domain'; + +export class InvalidTimelineParametersException extends CoreUnprocessableEntityException { + constructor(paramName: string) { + super(`Invalid ${paramName}`); + } +} diff --git a/libs/server/repository/core/domain/src/lib/interfaces/i-time-trackable.interface.ts b/libs/server/repository/core/domain/src/lib/interfaces/i-time-trackable.interface.ts new file mode 100644 index 00000000..49c48b39 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/interfaces/i-time-trackable.interface.ts @@ -0,0 +1,4 @@ +export interface ITimeTrackable { + createdAt: Date; + closedAt?: Date; +} diff --git a/libs/server/repository/core/domain/src/lib/interfaces/timeline-bucket-item.interface.ts b/libs/server/repository/core/domain/src/lib/interfaces/timeline-bucket-item.interface.ts new file mode 100644 index 00000000..4aea8d97 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/interfaces/timeline-bucket-item.interface.ts @@ -0,0 +1,4 @@ +export interface TimelineBucketItem { + entity: T; + timeIn: number; +} diff --git a/libs/server/repository/core/domain/src/lib/interfaces/timeline-bucket.interface.ts b/libs/server/repository/core/domain/src/lib/interfaces/timeline-bucket.interface.ts new file mode 100644 index 00000000..8af1193b --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/interfaces/timeline-bucket.interface.ts @@ -0,0 +1,5 @@ +import { TimelineBucketItem } from './timeline-bucket-item.interface'; + +export interface TimelineBuckets { + [label: number]: TimelineBucketItem[]; +} diff --git a/libs/server/repository/core/domain/src/lib/interfaces/timeline-date-range.interface.ts b/libs/server/repository/core/domain/src/lib/interfaces/timeline-date-range.interface.ts new file mode 100644 index 00000000..b7b3922b --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/interfaces/timeline-date-range.interface.ts @@ -0,0 +1,4 @@ +export interface TimelineDateRange { + dateFrom: Date; + dateTo: Date; +} diff --git a/libs/server/repository/core/domain/src/lib/interfaces/timeline-division-base.interface.ts b/libs/server/repository/core/domain/src/lib/interfaces/timeline-division-base.interface.ts new file mode 100644 index 00000000..393e6a84 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/interfaces/timeline-division-base.interface.ts @@ -0,0 +1,5 @@ +import { TimelineDateRange } from './timeline-date-range.interface'; + +export interface TimelineDivisionBase { + [timestamp: number]: TimelineDateRange; +} diff --git a/libs/server/repository/core/domain/src/lib/types/pr-state.type.ts b/libs/server/repository/core/domain/src/lib/types/pr-state.type.ts new file mode 100644 index 00000000..c2f886bb --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/types/pr-state.type.ts @@ -0,0 +1 @@ +export type PrStateType = 'open' | 'closed' | 'all'; diff --git a/libs/server/repository/core/domain/src/lib/types/timeline-step.type.ts b/libs/server/repository/core/domain/src/lib/types/timeline-step.type.ts new file mode 100644 index 00000000..9649362b --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/types/timeline-step.type.ts @@ -0,0 +1 @@ +export type TimelineStepType = 'day' | 'month' | 'week' | 'quarter' | 'year'; diff --git a/libs/server/repository/core/domain/src/lib/utils/iterator-to-array.util.ts b/libs/server/repository/core/domain/src/lib/utils/iterator-to-array.util.ts new file mode 100644 index 00000000..16c4aed0 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/iterator-to-array.util.ts @@ -0,0 +1,19 @@ +export function iteratorToArray(a: Iterator): T[] { + const result: T[] = []; + let current = a.next(); + while (!current.done) { + result.push(current.value); + current = a.next(); + } + return result; +} + +export async function asyncIteratorToArray(a: AsyncIterator): Promise { + const result: T[] = []; + let current = await a.next(); + while (!current.done) { + result.push(current.value); + current = await a.next(); + } + return result; +} diff --git a/libs/server/repository/core/domain/src/lib/utils/pad-number-to-places.util.spec.ts b/libs/server/repository/core/domain/src/lib/utils/pad-number-to-places.util.spec.ts new file mode 100644 index 00000000..26d7035f --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/pad-number-to-places.util.spec.ts @@ -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'); + }); +}); diff --git a/libs/server/repository/core/domain/src/lib/utils/pad-number-to-places.util.ts b/libs/server/repository/core/domain/src/lib/utils/pad-number-to-places.util.ts new file mode 100644 index 00000000..e7c63121 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/pad-number-to-places.util.ts @@ -0,0 +1,2 @@ +export const padNumberToPlaces = (num: number, places = 2): string => + String(num).padStart(places, '0'); diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/decrease-date-by-timeline-step.util.spec.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/decrease-date-by-timeline-step.util.spec.ts new file mode 100644 index 00000000..90432c0d --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/decrease-date-by-timeline-step.util.spec.ts @@ -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); + }); +}); diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/decrease-date-by-timeline-step.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/decrease-date-by-timeline-step.util.ts new file mode 100644 index 00000000..83c40b9b --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/decrease-date-by-timeline-step.util.ts @@ -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); + } +}; diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/divide-items-into-buckets.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/divide-items-into-buckets.util.ts new file mode 100644 index 00000000..cf8d6ffd --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/divide-items-into-buckets.util.ts @@ -0,0 +1,41 @@ +import { TimelineDivisionBase } from '../../interfaces/timeline-division-base.interface'; +import { TimelineBuckets } from '../../interfaces/timeline-bucket.interface'; +import { generateTimelineBuckets } from './generate-timeline-buckets.util'; +import { wrapInBucketItem } from './wrap-in-bucket-item.util'; +import { ITimeTrackable } from '../../interfaces/i-time-trackable.interface'; + +export function divideItemsIntoBuckets( + bucketRanges: TimelineDivisionBase, + items: T[] +): TimelineBuckets { + const buckets = generateTimelineBuckets(bucketRanges); + + items + .map(pr => wrapInBucketItem(pr)) + .forEach(current => { + Object.entries(bucketRanges) + .map(([ts, record]) => { + const lowerBound = Math.max( + record.dateFrom.getTime(), + current.entity.createdAt.getTime() + ); + const upperBound = current.entity.closedAt + ? Math.min(record.dateTo.getTime(), current.entity.closedAt.getTime()) + : record.dateTo.getTime(); + return { + labelTimestamp: Number(ts), + timeIn: upperBound - lowerBound, + rangeSize: record.dateTo.getTime() - record.dateFrom.getTime() + }; + }) + .filter(row => row.timeIn > 0) + .forEach(currentEntry => { + buckets[currentEntry.labelTimestamp].push({ + timeIn: currentEntry.timeIn, + entity: current.entity + }); + }); + }); + + return buckets; +} diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/format-date-with-step.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/format-date-with-step.util.ts new file mode 100644 index 00000000..b305672a --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/format-date-with-step.util.ts @@ -0,0 +1,17 @@ +import { TimelineStepType } from '@pimp-my-pr/server/repository/core/domain'; +import { padNumberToPlaces } from '../pad-number-to-places.util'; + +export const formatDateWithStep = (date: Date, step: TimelineStepType): string => { + switch (step) { + case 'year': + return String(date.getFullYear()); + case 'month': + return `${padNumberToPlaces(date.getMonth())}/${date.getFullYear()}`; + case 'day': + return `${padNumberToPlaces(date.getDay())}/${padNumberToPlaces(date.getMonth())}`; + case 'week': + return `${padNumberToPlaces(date.getDay())}+/${padNumberToPlaces(date.getMonth())}`; + case 'quarter': + return `${padNumberToPlaces(date.getMonth())}+/${date.getFullYear()}`; + } +}; diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/generate-stats.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/generate-stats.util.ts new file mode 100644 index 00000000..d04b87ae --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/generate-stats.util.ts @@ -0,0 +1,31 @@ +import { TimelineBuckets } from '../../interfaces/timeline-bucket.interface'; +import { TimelineDivisionBase } from '../../interfaces/timeline-division-base.interface'; +import { TimelineRecord } from '@pimp-my-pr/server/repository/core/domain'; +import { ITimeTrackable } from '../../interfaces/i-time-trackable.interface'; + +export function generateStats( + buckets: TimelineBuckets, + bucketRanges: TimelineDivisionBase +): TimelineRecord[] { + return Object.entries(buckets).map( + ([ts, entry]): TimelineRecord => { + const periodEnd = bucketRanges[ts].dateTo.getTime(); + const periodStart = bucketRanges[ts].dateFrom.getTime(); + const entityCount = entry.length; + const entitySumTimeIn = entry.reduce((total, item) => total + item.timeIn, 0); + const entitySumTimeOpened = entry.reduce((total, item) => { + const entityLocalPeriodEnd = item.entity.closedAt + ? Math.min(item.entity.closedAt.getTime(), periodEnd) + : periodEnd; + return total + entityLocalPeriodEnd - item.entity.createdAt.getTime(); + }, 0); + const avgWaitingTime = entityCount ? entitySumTimeOpened / entityCount : 0; + return { + dataFrom: new Date(Number(ts)), + sumCount: entityCount, + avgCount: entitySumTimeIn / (periodEnd - periodStart), + avgWaitingTime: avgWaitingTime + }; + } + ); +} diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/generate-timeline-bucket-ranges.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/generate-timeline-bucket-ranges.util.ts new file mode 100644 index 00000000..80535ec5 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/generate-timeline-bucket-ranges.util.ts @@ -0,0 +1,23 @@ +import { TimelineStepType } from '@pimp-my-pr/server/repository/core/domain'; +import { decreaseDateByTimelineStep } from './decrease-date-by-timeline-step.util'; +import { TimelineDateRange } from '../../interfaces/timeline-date-range.interface'; +import { TimelineDivisionBase } from '../../interfaces/timeline-division-base.interface'; + +export const generateTimelineBucketRanges = ( + date: Date, + entriesCount: number, + step: TimelineStepType +): TimelineDivisionBase => { + return Array.from(Array(entriesCount).keys()) + .map((key: number) => decreaseDateByTimelineStep(date, step, key + 1)) + .reduce( + (result, thisDate, index, array) => ({ + ...result, + [thisDate.getTime()]: { + dateFrom: thisDate, + dateTo: index ? array[index - 1] : date + } + }), + {} + ); +}; diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/generate-timeline-buckets.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/generate-timeline-buckets.util.ts new file mode 100644 index 00000000..f00adf43 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/generate-timeline-buckets.util.ts @@ -0,0 +1,12 @@ +import { TimelineDivisionBase } from '../../interfaces/timeline-division-base.interface'; +import { TimelineBuckets } from '../../interfaces/timeline-bucket.interface'; + +export function generateTimelineBuckets(div: TimelineDivisionBase): TimelineBuckets { + return Object.entries(div).reduce( + (total, [timestamp, _]) => ({ + ...total, + [Number(timestamp)]: [] + }), + {} + ); +} diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/get-step-beginning.util.spec.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/get-step-beginning.util.spec.ts new file mode 100644 index 00000000..9b116921 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/get-step-beginning.util.spec.ts @@ -0,0 +1,38 @@ +import { getStepBeginning } from './get-step-beginning.util'; + +describe('#getStepBeginning', () => { + it('should return day beginning', () => { + expect(getStepBeginning(new Date('1995-12-10T12:12:32'), 'day')).toEqual( + new Date('1995-12-10T00:00:00') + ); + }); + + it('should return week beginning', () => { + expect(getStepBeginning(new Date('1995-12-06T12:12:32'), 'week')).toEqual( + new Date('1995-12-03T00:00:00') + ); + }); + + it('should return month beginning', () => { + expect(getStepBeginning(new Date('1995-12-10T12:12:32'), 'month')).toEqual( + new Date('1995-12-01T00:00:00') + ); + }); + + it('should return quarter beginning', () => { + expect(getStepBeginning(new Date('1995-12-10T12:12:32'), 'quarter')).toEqual( + new Date('1995-09-01T00:00:00') + ); + }); + + it('should return year beginning', () => { + expect(getStepBeginning(new Date('1995-12-10T12:12:32'), 'year')).toEqual( + new Date('1995-01-01T00:00:00') + ); + }); + + it('should return new object', () => { + const date = new Date('1995-12-17T00:00:00'); + expect(getStepBeginning(date, 'day')).not.toBe(date); + }); +}); diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/get-step-beginning.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/get-step-beginning.util.ts new file mode 100644 index 00000000..96eaf534 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/get-step-beginning.util.ts @@ -0,0 +1,22 @@ +import { TimelineStepType } from '@pimp-my-pr/server/repository/core/domain'; +import { subtractDaysFromDate } from './subtract-days-from-date.util'; + +export const getStepBeginning = (date: Date, step: TimelineStepType): Date => { + let dateNew = new Date(date); + switch (step) { + case 'year': + dateNew.setMonth(0, 1); + break; + case 'quarter': + dateNew.setMonth(Math.floor(dateNew.getMonth() / 3) * 3 - 1, 1); + break; + case 'month': + dateNew.setDate(1); + break; + case 'week': + dateNew = subtractDaysFromDate(dateNew, dateNew.getDay()); + break; + } + dateNew.setHours(0, 0, 0, 0); + return dateNew; +}; diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/get-steps-count.util.spec.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/get-steps-count.util.spec.ts new file mode 100644 index 00000000..b29ccaac --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/get-steps-count.util.spec.ts @@ -0,0 +1,9 @@ +import { getStepsCount } from './get-steps-count.util'; + +describe('#getStepsCount', () => { + it('should return a correct number of steps', () => { + expect( + getStepsCount(new Date('1995-12-02T12:12:32'), new Date('1995-12-06T12:12:32'), 'day') + ).toBe(4); + }); +}); diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/get-steps-count.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/get-steps-count.util.ts new file mode 100644 index 00000000..79dfb381 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/get-steps-count.util.ts @@ -0,0 +1,14 @@ +import { TimelineStepType } from '@pimp-my-pr/server/repository/core/domain'; +import { getStepBeginning } from './get-step-beginning.util'; +import { decreaseDateByTimelineStep } from './decrease-date-by-timeline-step.util'; + +export const getStepsCount = (dateFrom: Date, dateTo: Date, step: TimelineStepType): number => { + let stepsCount = 0; + const fromStep = getStepBeginning(dateFrom, step); + let toStep = getStepBeginning(dateTo, step); + while (toStep.getTime() > fromStep.getTime()) { + toStep = decreaseDateByTimelineStep(toStep, step, 1); + stepsCount++; + } + return stepsCount ? stepsCount - 1 : 0; +}; diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/get-timeline-history.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/get-timeline-history.ts new file mode 100644 index 00000000..e49863e8 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/get-timeline-history.ts @@ -0,0 +1,18 @@ +import { TimelineRecord, TimelineStepType } from '@pimp-my-pr/server/repository/core/domain'; +import { generateTimelineBucketRanges } from './generate-timeline-bucket-ranges.util'; +import { getStepBeginning } from './get-step-beginning.util'; +import { divideItemsIntoBuckets } from './divide-items-into-buckets.util'; +import { generateStats } from './generate-stats.util'; +import { ITimeTrackable } from '../../interfaces/i-time-trackable.interface'; + +export function getTimeLineHistory( + items: T[], + step: TimelineStepType, + date: Date, + bucketsCount: number +): TimelineRecord[] { + const stepBegin = getStepBeginning(date, step); + const bucketRanges = generateTimelineBucketRanges(stepBegin, bucketsCount, step); + const buckets = divideItemsIntoBuckets(bucketRanges, items); + return generateStats(buckets, bucketRanges); +} diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-days-from-date.util.spec.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-days-from-date.util.spec.ts new file mode 100644 index 00000000..2ffa9d13 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-days-from-date.util.spec.ts @@ -0,0 +1,13 @@ +import { subtractDaysFromDate } from './subtract-days-from-date.util'; + +describe('#subtractDaysFromDate', () => { + it('should subtract n days', () => { + expect(subtractDaysFromDate(new Date('1995-12-17T00:00:00'), 7)).toEqual( + new Date('1995-12-10T00:00:00') + ); + }); + it('should return new object', () => { + const date = new Date('1995-12-17T00:00:00'); + expect(subtractDaysFromDate(date, 5)).not.toBe(date); + }); +}); diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-days-from-date.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-days-from-date.util.ts new file mode 100644 index 00000000..9796958a --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-days-from-date.util.ts @@ -0,0 +1,4 @@ +export const subtractDaysFromDate = (date: Date, days: number): Date => { + const day = 24 * 60 * 60 * 1000; + return new Date(date.getTime() - day * days); +}; diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-months-from-date.util.spec.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-months-from-date.util.spec.ts new file mode 100644 index 00000000..45c09757 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-months-from-date.util.spec.ts @@ -0,0 +1,18 @@ +import { subtractMonthsFromDate } from './subtract-months-from-date.util'; + +describe('#subtractMonthsFromDate', () => { + it('should subtract n months if there is no year-switch necessary', () => { + expect(subtractMonthsFromDate(new Date('1995-12-17T00:00:00'), 5)).toEqual( + new Date('1995-07-01T00:00:00') + ); + }); + it('should subtract n months if there is an year-switch necessary', () => { + expect(subtractMonthsFromDate(new Date('1995-01-17T00:00:00'), 2)).toEqual( + new Date('1994-11-01T00:00:00') + ); + }); + it('should return new object', () => { + const date = new Date('1995-12-17T00:00:00'); + expect(subtractMonthsFromDate(date, 5)).not.toBe(date); + }); +}); diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-months-from-date.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-months-from-date.util.ts new file mode 100644 index 00000000..77440e35 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/subtract-months-from-date.util.ts @@ -0,0 +1,7 @@ +export const subtractMonthsFromDate = (date: Date, nofMonths: number): Date => { + const thisMonth = date.getMonth(); + const newDate = new Date(date); + newDate.setMonth(thisMonth - nofMonths); + newDate.setDate(1); + return newDate; +}; diff --git a/libs/server/repository/core/domain/src/lib/utils/timeline/wrap-in-bucket-item.util.ts b/libs/server/repository/core/domain/src/lib/utils/timeline/wrap-in-bucket-item.util.ts new file mode 100644 index 00000000..f46ecd9f --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/timeline/wrap-in-bucket-item.util.ts @@ -0,0 +1,8 @@ +import { TimelineBucketItem } from '../../interfaces/timeline-bucket-item.interface'; + +export function wrapInBucketItem(item: T): TimelineBucketItem { + return { + timeIn: 0, + entity: item + }; +} diff --git a/libs/server/repository/core/domain/src/lib/utils/traverse-pages-until.util.ts b/libs/server/repository/core/domain/src/lib/utils/traverse-pages-until.util.ts new file mode 100644 index 00000000..707c50a2 --- /dev/null +++ b/libs/server/repository/core/domain/src/lib/utils/traverse-pages-until.util.ts @@ -0,0 +1,31 @@ +import { asyncIteratorToArray } from './iterator-to-array.util'; +import { ITimeTrackable } from '../interfaces/i-time-trackable.interface'; + +export type TraverseCallbackType = (page: number) => Promise; + +async function* getNextBatch( + callback: TraverseCallbackType, + onPage: number, + limit: Date +): AsyncIterator { + let page = 1; + while (true) { + const response = await callback(page++); + const hasReachedLimit = response[response.length - 1].createdAt.getTime() < limit.getTime(); + yield hasReachedLimit + ? response.filter(entity => entity.createdAt.getTime() > limit.getTime()) + : response; + if (response.length < onPage || hasReachedLimit) break; + } +} + +export async function traversePagesUntil( + callback: TraverseCallbackType, + onPage: number, + end: Date +): Promise { + return (await asyncIteratorToArray(getNextBatch(callback, onPage, end))).reduce( + (total, current) => [...total, ...current], + [] + ); +} diff --git a/libs/server/repository/infrastructure/src/lib/bitbucket/repositories/bitbucket-pr.repository.ts b/libs/server/repository/infrastructure/src/lib/bitbucket/repositories/bitbucket-pr.repository.ts index f9a7095c..319c6096 100644 --- a/libs/server/repository/infrastructure/src/lib/bitbucket/repositories/bitbucket-pr.repository.ts +++ b/libs/server/repository/infrastructure/src/lib/bitbucket/repositories/bitbucket-pr.repository.ts @@ -1,5 +1,5 @@ import { HttpService, Injectable } from '@nestjs/common'; -import { PrEntity } from '@pimp-my-pr/server/repository/core/domain'; +import { PrEntity, PrStateType } from '@pimp-my-pr/server/repository/core/domain'; import { PrRepository } from '@pimp-my-pr/server/repository/core/domain-services'; import { bitbucketConfig } from '@pimp-my-pr/server/shared/config'; import { catchRequestExceptions } from '@pimp-my-pr/server/shared/util-exception'; @@ -12,6 +12,7 @@ import { BitbucketPrDiffEntity } from '../domain/entities/bitbucket-pr-diff.enti import { BitbucketPrEntity } from '../domain/entities/bitbucket-pr.entity'; import { BitbucketPaginatedResponse } from '../domain/interfaces/bitbucket-paginated-response.interface'; import { mapBitbucketPrDetails } from '../mappers/map-bitbucket-pr-details'; +import { urlWithQueryParams } from '@pimp-my-pr/shared/domain'; @Injectable() export class BitbucketPrRepository extends PrRepository { @@ -26,11 +27,21 @@ export class BitbucketPrRepository extends PrRepository { super(); } - findByRepositoryId(repositoryId: string, token: string): Promise { + findByRepositoryId( + repositoryId: string, + token: string, + prState: PrStateType = 'open', + page = 1, + onPage = 50 + ): Promise { return this.getDataFromAllPages( this.httpService .get>( - this.endpoints.getRepositoryPrs.url({ repositoryId }), + urlWithQueryParams(this.endpoints.getRepositoryPrs.url({ repositoryId }), { + page: page, + pagelen: onPage, + state: this.mapPrStateToBitbucketPrState(prState) + }), { headers: { Authorization: `Bearer ${token}` } } ) .pipe(map(res => res.data)), @@ -44,6 +55,17 @@ export class BitbucketPrRepository extends PrRepository { .toPromise(); } + private mapPrStateToBitbucketPrState(state: PrStateType): string { + switch (state) { + case 'all': + return ''; + case 'closed': + return 'MERGED'; + case 'open': + return 'OPEN'; + } + } + private getPrsFromNextPage( token: string ): (nextPageUrl: string) => Observable { diff --git a/libs/server/repository/infrastructure/src/lib/github/repositories/github-pr.repository.ts b/libs/server/repository/infrastructure/src/lib/github/repositories/github-pr.repository.ts index 80337a57..e8c0cf99 100644 --- a/libs/server/repository/infrastructure/src/lib/github/repositories/github-pr.repository.ts +++ b/libs/server/repository/infrastructure/src/lib/github/repositories/github-pr.repository.ts @@ -5,10 +5,12 @@ import { githubConfig } from '@pimp-my-pr/server/shared/config'; import { catchRequestExceptions } from '@pimp-my-pr/server/shared/util-exception'; import { urlFactory } from '@valueadd/typed-urls'; import { forkJoin, of } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import { GithubPrDetailsEntity } from '../domain/entities/github-pr-details.entity'; import { GithubPrEntity } from '../domain/entities/github-pr.entity'; import { mapGithubPr } from '../mappers/map-github-pr'; +import { PrStateType } from '@pimp-my-pr/server/repository/core/domain'; +import { urlWithQueryParams } from '@pimp-my-pr/shared/domain'; @Injectable() export class GithubPrRepository extends PrRepository { @@ -37,14 +39,31 @@ export class GithubPrRepository extends PrRepository { .toPromise(); } - findByRepositoryId(repositoryId: string, token: string): Promise { + findByRepositoryId( + repositoryId: string, + token: string, + prState: PrStateType = 'open', + page = 1, + onPage = 50 + ): Promise { return this.httpService - .get(this.endpoints.getRepositoryPrs.url({ fullName: repositoryId }), { - headers: { Authorization: `token ${token}` } - }) + .get( + urlWithQueryParams( + this.endpoints.getRepositoryPrs.url({ + fullName: repositoryId + }), + { + page: page, + state: prState, + per_page: onPage + } + ), + { + headers: { Authorization: `token ${token}` } + } + ) .pipe( map(res => res.data), - switchMap(prs => prs.length ? forkJoin(prs.map(pr => this.get(repositoryId, pr.number, token))) : of([]) ) diff --git a/libs/server/repository/infrastructure/src/lib/gitlab/repositories/gitlab-pr.repository.ts b/libs/server/repository/infrastructure/src/lib/gitlab/repositories/gitlab-pr.repository.ts index 815eba16..dcc1bf7a 100644 --- a/libs/server/repository/infrastructure/src/lib/gitlab/repositories/gitlab-pr.repository.ts +++ b/libs/server/repository/infrastructure/src/lib/gitlab/repositories/gitlab-pr.repository.ts @@ -1,5 +1,5 @@ import { HttpService, Injectable } from '@nestjs/common'; -import { PrEntity } from '@pimp-my-pr/server/repository/core/domain'; +import { PrEntity, PrStateType } from '@pimp-my-pr/server/repository/core/domain'; import { PrRepository } from '@pimp-my-pr/server/repository/core/domain-services'; import { gitlabConfig } from '@pimp-my-pr/server/shared/config'; import { catchRequestExceptions } from '@pimp-my-pr/server/shared/util-exception'; @@ -9,6 +9,7 @@ import { map, switchMap } from 'rxjs/operators'; import { GitlabPrEntity } from '../domain/entities/gitlab-pr.entity'; import { mapGitlabPr } from '../mappers/map-gitalb-pr'; import { GitlabPrDetailsEntity } from '../domain/entities/gitlab-pr-details.entity'; +import { urlWithQueryParams } from '@pimp-my-pr/shared/domain'; @Injectable() export class GitlabPrRepository extends PrRepository { @@ -43,10 +44,23 @@ export class GitlabPrRepository extends PrRepository { .toPromise(); } - findByRepositoryId(repositoryId: string, token: string): Promise { + findByRepositoryId( + repositoryId: string, + token: string, + prState: PrStateType = 'open', + page = 1, + onPage = 50 + ): Promise { return this.httpService .get( - this.endpoints.getRepositoryPrs.url({ fullName: encodeURIComponent(repositoryId) }), + urlWithQueryParams( + this.endpoints.getRepositoryPrs.url({ fullName: encodeURIComponent(repositoryId) }), + { + page: page, + per_page: onPage, + state: prState !== 'open' ? prState : 'opened' + } + ), { headers: { Authorization: `Bearer ${token}` } } diff --git a/libs/shared/domain/src/index.ts b/libs/shared/domain/src/index.ts index 9e04872a..cf78abdd 100644 --- a/libs/shared/domain/src/index.ts +++ b/libs/shared/domain/src/index.ts @@ -20,3 +20,4 @@ export * from './lib/responses/login-success.response'; export * from './lib/responses/response.interface'; export * from './lib/responses/reviewer-statistics.response'; export * from './lib/responses/user-info.response'; +export * from './lib/util/url-with-query-params.util'; diff --git a/libs/shared/domain/src/lib/util/url-with-query-params.util.spec.ts b/libs/shared/domain/src/lib/util/url-with-query-params.util.spec.ts new file mode 100644 index 00000000..96e0a012 --- /dev/null +++ b/libs/shared/domain/src/lib/util/url-with-query-params.util.spec.ts @@ -0,0 +1,13 @@ +import { urlWithQueryParams } from '@pimp-my-pr/shared/domain'; + +describe('#urlWithQueryParams', () => { + it('should return correct url with params', () => { + expect( + urlWithQueryParams('url.com', { + a: 1, + b: 'test', + c: true + }) + ).toBe('url.com?a=1&b=test&c=true'); + }); +}); diff --git a/libs/shared/domain/src/lib/util/url-with-query-params.util.ts b/libs/shared/domain/src/lib/util/url-with-query-params.util.ts new file mode 100644 index 00000000..93768796 --- /dev/null +++ b/libs/shared/domain/src/lib/util/url-with-query-params.util.ts @@ -0,0 +1,11 @@ +export const urlWithQueryParams = ( + url: string, + queryParams: { [key: string]: number | string | boolean } +) => { + return Object.entries(queryParams).reduce((total, current, ind, arr) => { + if (ind === 0) total += '?'; + total += current.join('='); + if (ind !== arr.length - 1) total += '&'; + return total; + }, url); +};