diff --git a/src/lib/features/project-insights/createProjectInsightsService.ts b/src/lib/features/project-insights/createProjectInsightsService.ts index c2b65e101b..7550239fdf 100644 --- a/src/lib/features/project-insights/createProjectInsightsService.ts +++ b/src/lib/features/project-insights/createProjectInsightsService.ts @@ -12,6 +12,8 @@ import FeatureTypeStore from '../../db/feature-type-store'; import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store'; import { ProjectInsightsService } from './project-insights-service'; import ProjectStore from '../project/project-store'; +import { ProjectInsightsReadModel } from './project-insights-read-model'; +import { FakeProjectInsightsReadModel } from './fake-project-insights-read-model'; export const createProjectInsightsService = ( db: Db, @@ -34,6 +36,7 @@ export const createProjectInsightsService = ( const featureTypeStore = new FeatureTypeStore(db, getLogger); const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger); const featureToggleService = createFeatureToggleService(db, config); + const projectInsightsReadModel = new ProjectInsightsReadModel(db); return new ProjectInsightsService( { @@ -43,6 +46,7 @@ export const createProjectInsightsService = ( projectStatsStore, }, featureToggleService, + projectInsightsReadModel, ); }; @@ -54,6 +58,7 @@ export const createFakeProjectInsightsService = ( const featureTypeStore = new FakeFeatureTypeStore(); const projectStatsStore = new FakeProjectStatsStore(); const featureToggleService = createFakeFeatureToggleService(config); + const projectInsightsReadModel = new FakeProjectInsightsReadModel(); return new ProjectInsightsService( { @@ -63,5 +68,6 @@ export const createFakeProjectInsightsService = ( projectStatsStore, }, featureToggleService, + projectInsightsReadModel, ); }; diff --git a/src/lib/features/project-insights/fake-project-insights-read-model.ts b/src/lib/features/project-insights/fake-project-insights-read-model.ts new file mode 100644 index 0000000000..961ca03944 --- /dev/null +++ b/src/lib/features/project-insights/fake-project-insights-read-model.ts @@ -0,0 +1,19 @@ +import type { + ChangeRequestCounts, + IProjectInsightsReadModel, +} from './project-insights-read-model-type'; + +const changeRequestCounts: ChangeRequestCounts = { + total: 0, + approved: 0, + applied: 0, + rejected: 0, + reviewRequired: 10, + scheduled: 0, +}; + +export class FakeProjectInsightsReadModel implements IProjectInsightsReadModel { + async getChangeRequests(projectId: string): Promise { + return changeRequestCounts; + } +} diff --git a/src/lib/features/project-insights/project-insights-read-model-type.ts b/src/lib/features/project-insights/project-insights-read-model-type.ts new file mode 100644 index 0000000000..f100d77299 --- /dev/null +++ b/src/lib/features/project-insights/project-insights-read-model-type.ts @@ -0,0 +1,12 @@ +export type ChangeRequestCounts = { + total: number; + approved: number; + applied: number; + rejected: number; + reviewRequired: number; + scheduled: number; +}; + +export interface IProjectInsightsReadModel { + getChangeRequests(projectId: string): Promise; +} diff --git a/src/lib/features/project-insights/project-insights-read-model.ts b/src/lib/features/project-insights/project-insights-read-model.ts new file mode 100644 index 0000000000..122ce20c7c --- /dev/null +++ b/src/lib/features/project-insights/project-insights-read-model.ts @@ -0,0 +1,59 @@ +import type { + ChangeRequestCounts, + IProjectInsightsReadModel, +} from './project-insights-read-model-type'; +import type { Db } from '../../db/db'; + +export type ChangeRequestDBState = + | 'Approved' + | 'In review' + | 'Applied' + | 'Scheduled' + | 'Rejected'; + +export class ProjectInsightsReadModel implements IProjectInsightsReadModel { + private db: Db; + + constructor(db: Db) { + this.db = db; + } + + async getChangeRequests(projectId: string): Promise { + const changeRequestCounts: ChangeRequestCounts = { + total: 0, + approved: 0, + applied: 0, + rejected: 0, + reviewRequired: 0, + scheduled: 0, + }; + + const rows: Array<{ state: ChangeRequestDBState; count: string }> = + await this.db('change_requests') + .select('state') + .count('* as count') + .where('project', '=', projectId) + .groupBy('state'); + + return rows.reduce((acc, current) => { + if (current.state === 'Applied') { + acc.applied = Number(current.count); + acc.total += Number(current.count); + } else if (current.state === 'Approved') { + acc.approved = Number(current.count); + acc.total += Number(current.count); + } else if (current.state === 'Rejected') { + acc.rejected = Number(current.count); + acc.total += Number(current.count); + } else if (current.state === 'In review') { + acc.reviewRequired = Number(current.count); + acc.total += Number(current.count); + } else if (current.state === 'Scheduled') { + acc.scheduled = Number(current.count); + acc.total += Number(current.count); + } + + return acc; + }, changeRequestCounts); + } +} diff --git a/src/lib/features/project-insights/project-insights-service.ts b/src/lib/features/project-insights/project-insights-service.ts index a8f20ded20..a2aba73813 100644 --- a/src/lib/features/project-insights/project-insights-service.ts +++ b/src/lib/features/project-insights/project-insights-service.ts @@ -10,6 +10,7 @@ import { calculateAverageTimeToProd } from '../feature-toggle/time-to-production import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type'; import type { ProjectDoraMetricsSchema } from '../../openapi'; import { calculateProjectHealth } from '../../domain/project-health/project-health'; +import type { IProjectInsightsReadModel } from './project-insights-read-model-type'; export class ProjectInsightsService { private projectStore: IProjectStore; @@ -22,6 +23,8 @@ export class ProjectInsightsService { private projectStatsStore: IProjectStatsStore; + private projectInsightsReadModel: IProjectInsightsReadModel; + constructor( { projectStore, @@ -36,12 +39,14 @@ export class ProjectInsightsService { | 'featureTypeStore' >, featureToggleService: FeatureToggleService, + projectInsightsReadModel: IProjectInsightsReadModel, ) { this.projectStore = projectStore; this.featureToggleStore = featureToggleStore; this.featureTypeStore = featureTypeStore; this.featureToggleService = featureToggleService; this.projectStatsStore = projectStatsStore; + this.projectInsightsReadModel = projectInsightsReadModel; } private async getDoraMetrics( @@ -103,27 +108,28 @@ export class ProjectInsightsService { inactive: 3, totalPreviousMonth: 15, }, - changeRequests: { - total: 24, - approved: 5, - applied: 2, - rejected: 4, - reviewRequired: 10, - scheduled: 3, - }, }; - const [stats, featureTypeCounts, health, leadTime] = await Promise.all([ - this.projectStatsStore.getProjectStats(projectId), - this.featureToggleService.getFeatureTypeCounts({ - projectId, - archived: false, - }), - this.getHealthInsights(projectId), - this.getDoraMetrics(projectId), - ]); + const [stats, featureTypeCounts, health, leadTime, changeRequests] = + await Promise.all([ + this.projectStatsStore.getProjectStats(projectId), + this.featureToggleService.getFeatureTypeCounts({ + projectId, + archived: false, + }), + this.getHealthInsights(projectId), + this.getDoraMetrics(projectId), + this.projectInsightsReadModel.getChangeRequests(projectId), + ]); - return { ...result, stats, featureTypeCounts, health, leadTime }; + return { + ...result, + stats, + featureTypeCounts, + health, + leadTime, + changeRequests, + }; } private async getProjectHealth( diff --git a/src/lib/features/project-insights/projects-insights.e2e.test.ts b/src/lib/features/project-insights/projects-insights.e2e.test.ts index a7205c1cda..d84a2a7e7f 100644 --- a/src/lib/features/project-insights/projects-insights.e2e.test.ts +++ b/src/lib/features/project-insights/projects-insights.e2e.test.ts @@ -34,6 +34,8 @@ test('project insights happy path', async () => { .expect('Content-Type', /json/) .expect(200); + console.log(body); + expect(body).toMatchObject({ stats: { avgTimeToProdCurrentWindow: 0, @@ -53,5 +55,13 @@ test('project insights happy path', async () => { staleCount: 0, rating: 100, }, + changeRequests: { + total: 0, + approved: 0, + applied: 0, + rejected: 0, + reviewRequired: 0, + scheduled: 0, + }, }); }); diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts index 085a84b8d9..a6ce3f78a3 100644 --- a/src/lib/features/project/project-service.ts +++ b/src/lib/features/project/project-service.ts @@ -76,7 +76,6 @@ import type { IProjectEnterpriseSettingsUpdate, IProjectQuery, } from './project-store-type'; -import { calculateProjectHealth } from '../../domain/project-health/project-health'; const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown'; @@ -1240,62 +1239,6 @@ export default class ProjectService { }; } - private async getHealthInsights(projectId: string) { - const [overview, featureTypes] = await Promise.all([ - this.getProjectHealth(projectId, false, undefined), - this.featureTypeStore.getAll(), - ]); - - const { activeCount, potentiallyStaleCount, staleCount } = - calculateProjectHealth(overview.features, featureTypes); - - return { - activeCount, - potentiallyStaleCount, - staleCount, - rating: overview.health, - }; - } - - async getProjectInsights(projectId: string) { - const result = { - leadTime: { - projectAverage: 17.1, - features: [ - { name: 'feature1', timeToProduction: 120 }, - { name: 'feature2', timeToProduction: 0 }, - { name: 'feature3', timeToProduction: 33 }, - { name: 'feature4', timeToProduction: 131 }, - { name: 'feature5', timeToProduction: 2 }, - ], - }, - members: { - active: 20, - inactive: 3, - totalPreviousMonth: 15, - }, - changeRequests: { - total: 24, - approved: 5, - applied: 2, - rejected: 4, - reviewRequired: 10, - scheduled: 3, - }, - }; - - const [stats, featureTypeCounts, health] = await Promise.all([ - this.projectStatsStore.getProjectStats(projectId), - this.featureToggleService.getFeatureTypeCounts({ - projectId, - archived: false, - }), - this.getHealthInsights(projectId), - ]); - - return { ...result, stats, featureTypeCounts, health }; - } - async getProjectHealth( projectId: string, archived: boolean = false,