Skip to content

Commit

Permalink
chore: put project lifecycle read model in own directory + add fake (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
thomasheartman authored Nov 8, 2024
1 parent 8f18c2b commit f92441f
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -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();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {
IProjectLifecycleSummaryReadModel,
ProjectLifecycleSummary,
} from './project-lifecycle-read-model-type';

export class FakeProjectLifecycleSummaryReadModel
implements IProjectLifecycleSummaryReadModel
{
async getProjectLifecycleSummary(): Promise<ProjectLifecycleSummary> {
const placeholderData = {
averageDays: 0,
currentFlags: 0,
};
return {
initial: placeholderData,
preLive: placeholderData,
live: placeholderData,
completed: placeholderData,
archived: {
currentFlags: 0,
last30Days: 0,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface IProjectLifecycleSummaryReadModel {
getProjectLifecycleSummary(
projectId: string,
): Promise<ProjectLifecycleSummary>;
}

type StageDataWithAverageDays = {
averageDays: number | null;
currentFlags: number;
};

export type ProjectLifecycleSummary = {
initial: StageDataWithAverageDays;
preLive: StageDataWithAverageDays;
live: StageDataWithAverageDays;
completed: StageDataWithAverageDays;
archived: {
currentFlags: number;
last30Days: number;
};
};
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -93,8 +100,6 @@ describe('Average time calculation', () => {
}
}

const readModel = new ProjectLifecycleSummaryReadModel(db.rawDatabase);

const result = await readModel.getAverageTimeInEachStage(project.id);

expect(result).toMatchObject({
Expand All @@ -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);

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AverageTimeInStage> {
const q = this.db
.with(
'stage_durations',
Expand Down Expand Up @@ -88,7 +80,7 @@ export class ProjectLifecycleSummaryReadModel
);
}

async getCurrentFlagsInEachStage(projectId: string) {
async getCurrentFlagsInEachStage(projectId: string): Promise<FlagsInStage> {
const query = this.db('feature_lifecycles as fl')
.innerJoin('features as f', 'fl.feature', 'f.name')
.where('f.project', projectId)
Expand All @@ -110,11 +102,18 @@ export class ProjectLifecycleSummaryReadModel
completed: 0,
archived: 0,
},
);
) as FlagsInStage;
}

async getArchivedFlagsOverLastMonth(projectId: string) {
return 0;
async getArchivedFlagsLast30Days(projectId: string): Promise<number> {
const dateMinusThirtyDays = subDays(new Date(), 30).toISOString();

return this.featureToggleStore.countByDate({
project: projectId,
archived: true,
dateAccessor: 'archived_at',
date: dateMinusThirtyDays,
});
}

async getProjectLifecycleSummary(
Expand All @@ -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,
},
};
}
Expand Down

0 comments on commit f92441f

Please sign in to comment.