1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00

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.
This commit is contained in:
Thomas Heartman 2024-11-08 12:33:03 +01:00 committed by GitHub
parent 8f18c2b194
commit f92441fa50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 129 additions and 60 deletions

View File

@ -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();
};

View File

@ -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,
},
};
}
}

View File

@ -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;
};
};

View File

@ -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({

View File

@ -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<AverageTimeInStage> {
const q = this.db
.with(
'stage_durations',
@ -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)
@ -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(
@ -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,
},
};
}