From f92441fa50eb5c2f6ab7155bf15c8607b01fe392 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 8 Nov 2024 12:33:03 +0100 Subject: [PATCH] chore: put project lifecycle read model in own directory + add fake (#8700) This PR moves the project lifecycle summary to its own subdirectory and adds files for types (interface) and a fake implementation. It also adds a query for archived flags within the last 30 days taken from `getStatusUpdates` in `src/lib/features/project/project-service.ts` and maps the gathered data onto the expected structure. The expected types have also been adjusted to account for no data. Next step will be hooking it up to the project status service, adding schema, and exposing it in the controller. --- .../createProjectLifecycleSummaryReadModel.ts | 24 +++++ ...ke-project-lifecycle-summary-read-model.ts | 25 +++++ .../project-lifecycle-read-model-type.ts | 21 ++++ ...oject-lifecycle-summary-read-model.test.ts | 21 ++-- .../project-lifecycle-summary-read-model.ts | 98 +++++++++---------- 5 files changed, 129 insertions(+), 60 deletions(-) create mode 100644 src/lib/features/project-status/project-lifecycle-read-model/createProjectLifecycleSummaryReadModel.ts create mode 100644 src/lib/features/project-status/project-lifecycle-read-model/fake-project-lifecycle-summary-read-model.ts create mode 100644 src/lib/features/project-status/project-lifecycle-read-model/project-lifecycle-read-model-type.ts rename src/lib/features/project-status/{ => project-lifecycle-read-model}/project-lifecycle-summary-read-model.test.ts (93%) rename src/lib/features/project-status/{ => project-lifecycle-read-model}/project-lifecycle-summary-read-model.ts (60%) 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 000000000000..ca98027cc41b --- /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 000000000000..a9a8c10e1a49 --- /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 000000000000..bd8d2478c937 --- /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 95169e7fe2e9..b38bfaa4abde 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 f9c861d9ab2d..d81c2db1e284 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; - -export type IProjectLifecycleSummaryReadModel = {}; +type FlagsInStage = { + initial: number; + 'pre-live': number; + live: number; + completed: number; + archived: number; +}; -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, }, }; }