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';
|
import type { IFeatureLifecycleStage } from '../../types';
|
||||||
|
|
||||||
export class FakeFeatureLifecycleReadModel
|
export class FakeFeatureLifecycleReadModel
|
||||||
implements IFeatureLifecycleReadModel
|
implements IFeatureLifecycleReadModel
|
||||||
{
|
{
|
||||||
|
getStageCount(): Promise<StageCount[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
getStageCountByProject(): Promise<StageCountByProject[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
findCurrentStage(
|
findCurrentStage(
|
||||||
feature: string,
|
feature: string,
|
||||||
): Promise<IFeatureLifecycleStage | undefined> {
|
): Promise<IFeatureLifecycleStage | undefined> {
|
||||||
|
@ -57,6 +57,10 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
|
|||||||
this.lifecycles[feature] = [];
|
this.lifecycles[feature] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<void> {
|
||||||
|
this.lifecycles = {};
|
||||||
|
}
|
||||||
|
|
||||||
async stageExists(stage: FeatureLifecycleStage): Promise<boolean> {
|
async stageExists(stage: FeatureLifecycleStage): Promise<boolean> {
|
||||||
const lifecycle = await this.get(stage.feature);
|
const lifecycle = await this.get(stage.feature);
|
||||||
return Boolean(lifecycle.find((s) => s.stage === stage.stage));
|
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 {
|
export interface IFeatureLifecycleReadModel {
|
||||||
findCurrentStage(
|
findCurrentStage(
|
||||||
feature: string,
|
feature: string,
|
||||||
): Promise<IFeatureLifecycleStage | undefined>;
|
): 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 { 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 { getCurrentStage } from './get-current-stage';
|
||||||
import type { IFeatureLifecycleStage, StageName } from '../../types';
|
import type { IFeatureLifecycleStage, StageName } from '../../types';
|
||||||
|
|
||||||
@ -17,6 +21,61 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
|
|||||||
this.db = db;
|
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(
|
async findCurrentStage(
|
||||||
feature: string,
|
feature: string,
|
||||||
): Promise<IFeatureLifecycleStage | undefined> {
|
): Promise<IFeatureLifecycleStage | undefined> {
|
||||||
|
@ -20,6 +20,7 @@ export interface IFeatureLifecycleStore {
|
|||||||
getAll(): Promise<FeatureLifecycleProjectItem[]>;
|
getAll(): Promise<FeatureLifecycleProjectItem[]>;
|
||||||
stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
|
stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
|
||||||
delete(feature: string): Promise<void>;
|
delete(feature: string): Promise<void>;
|
||||||
|
deleteAll(): Promise<void>;
|
||||||
deleteStage(stage: FeatureLifecycleStage): Promise<void>;
|
deleteStage(stage: FeatureLifecycleStage): Promise<void>;
|
||||||
backfill(): Promise<void>;
|
backfill(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -101,6 +101,10 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore {
|
|||||||
await this.db('feature_lifecycles').where({ feature }).del();
|
await this.db('feature_lifecycles').where({ feature }).del();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<void> {
|
||||||
|
await this.db('feature_lifecycles').del();
|
||||||
|
}
|
||||||
|
|
||||||
async deleteStage(stage: FeatureLifecycleStage): Promise<void> {
|
async deleteStage(stage: FeatureLifecycleStage): Promise<void> {
|
||||||
await this.db('feature_lifecycles')
|
await this.db('feature_lifecycles')
|
||||||
.where({
|
.where({
|
||||||
|
@ -62,7 +62,9 @@ afterAll(async () => {
|
|||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {});
|
beforeEach(async () => {
|
||||||
|
await featureLifecycleStore.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
const getFeatureLifecycle = async (featureName: string, expectedCode = 200) => {
|
const getFeatureLifecycle = async (featureName: string, expectedCode = 200) => {
|
||||||
return app.request
|
return app.request
|
||||||
|
Loading…
Reference in New Issue
Block a user