1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: lifecycle stage count (#7434)

This commit is contained in:
Mateusz Kwasniewski 2024-06-25 09:11:46 +02:00 committed by GitHub
parent 26d125b495
commit c14c67f476
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 159 additions and 4 deletions

View File

@ -1,9 +1,19 @@
import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type';
import type {
IFeatureLifecycleReadModel,
StageCount,
StageCountByProject,
} from './feature-lifecycle-read-model-type';
import type { IFeatureLifecycleStage } from '../../types';
export class FakeFeatureLifecycleReadModel
implements IFeatureLifecycleReadModel
{
getStageCount(): Promise<StageCount[]> {
return Promise.resolve([]);
}
getStageCountByProject(): Promise<StageCountByProject[]> {
return Promise.resolve([]);
}
findCurrentStage(
feature: string,
): Promise<IFeatureLifecycleStage | undefined> {

View File

@ -57,6 +57,10 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
this.lifecycles[feature] = [];
}
async deleteAll(): Promise<void> {
this.lifecycles = {};
}
async stageExists(stage: FeatureLifecycleStage): Promise<boolean> {
const lifecycle = await this.get(stage.feature);
return Boolean(lifecycle.find((s) => s.stage === stage.stage));

View File

@ -1,7 +1,18 @@
import type { IFeatureLifecycleStage } from '../../types';
import type { IFeatureLifecycleStage, StageName } from '../../types';
export type StageCount = {
stage: StageName;
count: number;
};
export type StageCountByProject = StageCount & {
project: string;
};
export interface IFeatureLifecycleReadModel {
findCurrentStage(
feature: string,
): Promise<IFeatureLifecycleStage | undefined>;
getStageCount(): Promise<StageCount[]>;
getStageCountByProject(): Promise<StageCountByProject[]>;
}

View File

@ -0,0 +1,64 @@
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import getLogger from '../../../test/fixtures/no-logger';
import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model';
import type { IFeatureLifecycleStore } from './feature-lifecycle-store-type';
import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type';
import type { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type';
let db: ITestDb;
let featureLifeycycleReadModel: IFeatureLifecycleReadModel;
let featureLifecycleStore: IFeatureLifecycleStore;
let featureToggleStore: IFeatureToggleStore;
beforeAll(async () => {
db = await dbInit('feature_lifecycle_read_model', getLogger);
featureLifeycycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
featureLifecycleStore = db.stores.featureLifecycleStore;
featureToggleStore = db.stores.featureToggleStore;
});
afterAll(async () => {
if (db) {
await db.destroy();
}
});
beforeEach(async () => {
await featureToggleStore.deleteAll();
});
test('can return stage count', async () => {
await featureToggleStore.create('default', {
name: 'featureA',
createdByUserId: 9999,
});
await featureToggleStore.create('default', {
name: 'featureB',
createdByUserId: 9999,
});
await featureToggleStore.create('default', {
name: 'featureC',
createdByUserId: 9999,
});
await featureLifecycleStore.insert([
{ feature: 'featureA', stage: 'initial' },
{ feature: 'featureB', stage: 'initial' },
{ feature: 'featureC', stage: 'initial' },
]);
await featureLifecycleStore.insert([
{ feature: 'featureA', stage: 'pre-live' },
]);
const stageCount = await featureLifeycycleReadModel.getStageCount();
expect(stageCount).toMatchObject([
{ stage: 'pre-live', count: 1 },
{ stage: 'initial', count: 2 },
]);
const stageCountByProject =
await featureLifeycycleReadModel.getStageCountByProject();
expect(stageCountByProject).toMatchObject([
{ project: 'default', stage: 'pre-live', count: 1 },
{ project: 'default', stage: 'initial', count: 2 },
]);
});

View File

@ -1,5 +1,9 @@
import type { Db } from '../../db/db';
import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type';
import type {
IFeatureLifecycleReadModel,
StageCount,
StageCountByProject,
} from './feature-lifecycle-read-model-type';
import { getCurrentStage } from './get-current-stage';
import type { IFeatureLifecycleStage, StageName } from '../../types';
@ -17,6 +21,61 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
this.db = db;
}
async getStageCount(): Promise<StageCount[]> {
const { rows } = await this.db.raw(`
SELECT
stage,
COUNT(*) AS feature_count
FROM (
SELECT DISTINCT ON (feature)
feature,
stage,
created_at
FROM
feature_lifecycles
ORDER BY
feature, created_at DESC
) AS LatestStages
GROUP BY
stage;
`);
return rows.map((row) => ({
stage: row.stage,
count: Number(row.feature_count),
}));
}
async getStageCountByProject(): Promise<StageCountByProject[]> {
const { rows } = await this.db.raw(`
SELECT
f.project,
ls.stage,
COUNT(*) AS feature_count
FROM (
SELECT DISTINCT ON (fl.feature)
fl.feature,
fl.stage,
fl.created_at
FROM
feature_lifecycles fl
ORDER BY
fl.feature, fl.created_at DESC
) AS ls
JOIN
features f ON f.name = ls.feature
GROUP BY
f.project,
ls.stage;
`);
return rows.map((row) => ({
stage: row.stage,
count: Number(row.feature_count),
project: row.project,
}));
}
async findCurrentStage(
feature: string,
): Promise<IFeatureLifecycleStage | undefined> {

View File

@ -20,6 +20,7 @@ export interface IFeatureLifecycleStore {
getAll(): Promise<FeatureLifecycleProjectItem[]>;
stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
delete(feature: string): Promise<void>;
deleteAll(): Promise<void>;
deleteStage(stage: FeatureLifecycleStage): Promise<void>;
backfill(): Promise<void>;
}

View File

@ -101,6 +101,10 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore {
await this.db('feature_lifecycles').where({ feature }).del();
}
async deleteAll(): Promise<void> {
await this.db('feature_lifecycles').del();
}
async deleteStage(stage: FeatureLifecycleStage): Promise<void> {
await this.db('feature_lifecycles')
.where({

View File

@ -62,7 +62,9 @@ afterAll(async () => {
await db.destroy();
});
beforeEach(async () => {});
beforeEach(async () => {
await featureLifecycleStore.deleteAll();
});
const getFeatureLifecycle = async (featureName: string, expectedCode = 200) => {
return app.request