Skip to content

Commit

Permalink
feat: lifecycle stage count (#7434)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Jun 25, 2024
1 parent 26d125b commit c14c67f
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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[]>;
}
Original file line number Diff line number Diff line change
@@ -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 },
]);
});
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
4 changes: 4 additions & 0 deletions src/lib/features/feature-lifecycle/feature-lifecycle-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c14c67f

Please sign in to comment.