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:
parent
26d125b495
commit
c14c67f476
@ -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> {
|
||||
|
@ -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));
|
||||
|
@ -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[]>;
|
||||
}
|
||||
|
@ -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 },
|
||||
]);
|
||||
});
|
@ -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> {
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user