diff --git a/src/lib/features/project-status/project-lifecycle-read-model/createProjectLifecycleSummaryReadModel.ts b/src/lib/features/project-status/project-lifecycle-read-model/createProjectLifecycleSummaryReadModel.ts new file mode 100644 index 0000000000..ca98027cc4 --- /dev/null +++ b/src/lib/features/project-status/project-lifecycle-read-model/createProjectLifecycleSummaryReadModel.ts @@ -0,0 +1,24 @@ +import type { Db, IUnleashConfig } from '../../../server-impl'; +import FeatureToggleStore from '../../feature-toggle/feature-toggle-store'; +import { FakeProjectLifecycleSummaryReadModel } from './fake-project-lifecycle-summary-read-model'; +import type { IProjectLifecycleSummaryReadModel } from './project-lifecycle-read-model-type'; +import { ProjectLifecycleSummaryReadModel } from './project-lifecycle-summary-read-model'; + +export const createProjectLifecycleSummaryReadModel = ( + db: Db, + config: IUnleashConfig, +): IProjectLifecycleSummaryReadModel => { + const { eventBus, getLogger, flagResolver } = config; + const featureToggleStore = new FeatureToggleStore( + db, + eventBus, + getLogger, + flagResolver, + ); + return new ProjectLifecycleSummaryReadModel(db, featureToggleStore); +}; + +export const createFakeProjectLifecycleSummaryReadModel = + (): IProjectLifecycleSummaryReadModel => { + return new FakeProjectLifecycleSummaryReadModel(); + }; diff --git a/src/lib/features/project-status/project-lifecycle-read-model/fake-project-lifecycle-summary-read-model.ts b/src/lib/features/project-status/project-lifecycle-read-model/fake-project-lifecycle-summary-read-model.ts new file mode 100644 index 0000000000..a9a8c10e1a --- /dev/null +++ b/src/lib/features/project-status/project-lifecycle-read-model/fake-project-lifecycle-summary-read-model.ts @@ -0,0 +1,25 @@ +import type { + IProjectLifecycleSummaryReadModel, + ProjectLifecycleSummary, +} from './project-lifecycle-read-model-type'; + +export class FakeProjectLifecycleSummaryReadModel + implements IProjectLifecycleSummaryReadModel +{ + async getProjectLifecycleSummary(): Promise { + const placeholderData = { + averageDays: 0, + currentFlags: 0, + }; + return { + initial: placeholderData, + preLive: placeholderData, + live: placeholderData, + completed: placeholderData, + archived: { + currentFlags: 0, + last30Days: 0, + }, + }; + } +} diff --git a/src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-read-model-type.ts b/src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-read-model-type.ts new file mode 100644 index 0000000000..bd8d2478c9 --- /dev/null +++ b/src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-read-model-type.ts @@ -0,0 +1,21 @@ +export interface IProjectLifecycleSummaryReadModel { + getProjectLifecycleSummary( + projectId: string, + ): Promise; +} + +type StageDataWithAverageDays = { + averageDays: number | null; + currentFlags: number; +}; + +export type ProjectLifecycleSummary = { + initial: StageDataWithAverageDays; + preLive: StageDataWithAverageDays; + live: StageDataWithAverageDays; + completed: StageDataWithAverageDays; + archived: { + currentFlags: number; + last30Days: number; + }; +}; diff --git a/src/lib/features/project-status/project-lifecycle-summary-read-model.test.ts b/src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-summary-read-model.test.ts similarity index 93% rename from src/lib/features/project-status/project-lifecycle-summary-read-model.test.ts rename to src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-summary-read-model.test.ts index 95169e7fe2..b38bfaa4ab 100644 --- a/src/lib/features/project-status/project-lifecycle-summary-read-model.test.ts +++ b/src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-summary-read-model.test.ts @@ -1,14 +1,21 @@ import { addDays, addMinutes } from 'date-fns'; -import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; -import getLogger from '../../../test/fixtures/no-logger'; +import dbInit, { + type ITestDb, +} from '../../../../test/e2e/helpers/database-init'; +import getLogger from '../../../../test/fixtures/no-logger'; import { ProjectLifecycleSummaryReadModel } from './project-lifecycle-summary-read-model'; -import type { StageName } from '../../types'; -import { randomId } from '../../util'; +import type { IFeatureToggleStore, StageName } from '../../../types'; +import { randomId } from '../../../util'; let db: ITestDb; +let readModel: ProjectLifecycleSummaryReadModel; beforeAll(async () => { db = await dbInit('project_lifecycle_summary_read_model_serial', getLogger); + readModel = new ProjectLifecycleSummaryReadModel( + db.rawDatabase, + {} as unknown as IFeatureToggleStore, + ); }); afterAll(async () => { @@ -93,8 +100,6 @@ describe('Average time calculation', () => { } } - const readModel = new ProjectLifecycleSummaryReadModel(db.rawDatabase); - const result = await readModel.getAverageTimeInEachStage(project.id); expect(result).toMatchObject({ @@ -110,7 +115,6 @@ describe('Average time calculation', () => { name: 'project', id: randomId(), }); - const readModel = new ProjectLifecycleSummaryReadModel(db.rawDatabase); const result1 = await readModel.getAverageTimeInEachStage(project.id); @@ -160,7 +164,6 @@ describe('Average time calculation', () => { name: 'project', id: randomId(), }); - const readModel = new ProjectLifecycleSummaryReadModel(db.rawDatabase); const flag = await db.stores.featureToggleStore.create(project.id, { name: randomId(), @@ -260,8 +263,6 @@ describe('count current flags in each stage', () => { }, ]); - const readModel = new ProjectLifecycleSummaryReadModel(db.rawDatabase); - const result = await readModel.getCurrentFlagsInEachStage(project.id); expect(result).toMatchObject({ diff --git a/src/lib/features/project-status/project-lifecycle-summary-read-model.ts b/src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-summary-read-model.ts similarity index 60% rename from src/lib/features/project-status/project-lifecycle-summary-read-model.ts rename to src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-summary-read-model.ts index f9c861d9ab..d81c2db1e2 100644 --- a/src/lib/features/project-status/project-lifecycle-summary-read-model.ts +++ b/src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-summary-read-model.ts @@ -1,48 +1,40 @@ -import * as permissions from '../../types/permissions'; -import type { Db } from '../../db/db'; +import type { Db } from '../../../db/db'; +import type { IFeatureToggleStore } from '../../../types'; +import { subDays } from 'date-fns'; +import type { + IProjectLifecycleSummaryReadModel, + ProjectLifecycleSummary, +} from './project-lifecycle-read-model-type'; -const { ADMIN } = permissions; +type FlagsInStage = { + initial: number; + 'pre-live': number; + live: number; + completed: number; + archived: number; +}; -export type IProjectLifecycleSummaryReadModel = {}; - -type ProjectLifecycleSummary = { - initial: { - averageDays: number; - currentFlags: number; - }; - preLive: { - averageDays: number; - currentFlags: number; - }; - live: { - averageDays: number; - currentFlags: number; - }; - completed: { - averageDays: number; - currentFlags: number; - }; - archived: { - currentFlags: number; - archivedFlagsOverLastMonth: number; - }; +type AverageTimeInStage = { + initial: number | null; + 'pre-live': number | null; + live: number | null; + completed: number | null; }; export class ProjectLifecycleSummaryReadModel implements IProjectLifecycleSummaryReadModel { private db: Db; + private featureToggleStore: IFeatureToggleStore; - constructor(db: Db) { + constructor(db: Db, featureToggleStore: IFeatureToggleStore) { this.db = db; + this.featureToggleStore = featureToggleStore; } - async getAverageTimeInEachStage(projectId: string): Promise<{ - initial: number | null; - 'pre-live': number | null; - live: number | null; - completed: number | null; - }> { + async getAverageTimeInEachStage( + projectId: string, + ): Promise { const q = this.db .with( 'stage_durations', @@ -88,7 +80,7 @@ export class ProjectLifecycleSummaryReadModel ); } - async getCurrentFlagsInEachStage(projectId: string) { + async getCurrentFlagsInEachStage(projectId: string): Promise { const query = this.db('feature_lifecycles as fl') .innerJoin('features as f', 'fl.feature', 'f.name') .where('f.project', projectId) @@ -110,11 +102,18 @@ export class ProjectLifecycleSummaryReadModel completed: 0, archived: 0, }, - ); + ) as FlagsInStage; } - async getArchivedFlagsOverLastMonth(projectId: string) { - return 0; + async getArchivedFlagsLast30Days(projectId: string): Promise { + const dateMinusThirtyDays = subDays(new Date(), 30).toISOString(); + + return this.featureToggleStore.countByDate({ + project: projectId, + archived: true, + dateAccessor: 'archived_at', + date: dateMinusThirtyDays, + }); } async getProjectLifecycleSummary( @@ -123,34 +122,33 @@ export class ProjectLifecycleSummaryReadModel const [ averageTimeInEachStage, currentFlagsInEachStage, - archivedFlagsOverLastMonth, + archivedFlagsLast30Days, ] = await Promise.all([ this.getAverageTimeInEachStage(projectId), this.getCurrentFlagsInEachStage(projectId), - this.getArchivedFlagsOverLastMonth(projectId), + this.getArchivedFlagsLast30Days(projectId), ]); - // collate the data return { initial: { - averageDays: 0, - currentFlags: 0, + averageDays: averageTimeInEachStage.initial, + currentFlags: currentFlagsInEachStage.initial, }, preLive: { - averageDays: 0, - currentFlags: 0, + averageDays: averageTimeInEachStage['pre-live'], + currentFlags: currentFlagsInEachStage['pre-live'], }, live: { - averageDays: 0, - currentFlags: 0, + averageDays: averageTimeInEachStage.live, + currentFlags: currentFlagsInEachStage.live, }, completed: { - averageDays: 0, - currentFlags: 0, + averageDays: averageTimeInEachStage.completed, + currentFlags: currentFlagsInEachStage.completed, }, archived: { - currentFlags: 0, - archivedFlagsOverLastMonth: 0, + currentFlags: currentFlagsInEachStage.archived, + last30Days: archivedFlagsLast30Days, }, }; }