mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: lifecycle count query (#9824)
This commit is contained in:
parent
26f582db21
commit
9911fe89be
@ -181,10 +181,7 @@ export const createStores = (
|
||||
featureStrategiesReadModel: new FeatureStrategiesReadModel(db),
|
||||
onboardingReadModel: createOnboardingReadModel(db),
|
||||
onboardingStore: new OnboardingStore(db),
|
||||
featureLifecycleReadModel: new FeatureLifecycleReadModel(
|
||||
db,
|
||||
config.flagResolver,
|
||||
),
|
||||
featureLifecycleReadModel: new FeatureLifecycleReadModel(db),
|
||||
largestResourcesReadModel: new LargestResourcesReadModel(db),
|
||||
integrationEventsStore: new IntegrationEventsStore(db, { eventBus }),
|
||||
featureCollaboratorsReadModel: new FeatureCollaboratorsReadModel(db),
|
||||
|
@ -0,0 +1,80 @@
|
||||
import {
|
||||
type IFeatureLifecycleReadModel,
|
||||
type IUnleashConfig,
|
||||
type IUnleashServices,
|
||||
NONE,
|
||||
} from '../../types';
|
||||
import type { OpenApiService } from '../../services';
|
||||
import { createResponseSchema, getStandardResponses } from '../../openapi';
|
||||
import Controller from '../../routes/controller';
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
type FeatureLifecycleCountSchema,
|
||||
featureLifecycleCountSchema,
|
||||
} from '../../openapi/spec/feature-lifecycle-count-schema';
|
||||
|
||||
export default class FeatureLifecycleCountController extends Controller {
|
||||
private featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
openApiService,
|
||||
featureLifecycleReadModel,
|
||||
}: Pick<
|
||||
IUnleashServices,
|
||||
'openApiService' | 'featureLifecycleReadModel'
|
||||
>,
|
||||
) {
|
||||
super(config);
|
||||
this.featureLifecycleReadModel = featureLifecycleReadModel;
|
||||
this.openApiService = openApiService;
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/count',
|
||||
handler: this.getStageCount,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Unstable'],
|
||||
summary: 'Get all features lifecycle stage count',
|
||||
description:
|
||||
'Information about the number of features in each lifecycle stage.',
|
||||
operationId: 'getFeatureLifecycleStageCount',
|
||||
responses: {
|
||||
200: createResponseSchema(
|
||||
'featureLifecycleCountSchema',
|
||||
),
|
||||
...getStandardResponses(401, 403, 404),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getStageCount(
|
||||
_: Request<any, any, any, any>,
|
||||
res: Response<FeatureLifecycleCountSchema>,
|
||||
): Promise<void> {
|
||||
const stageCounts =
|
||||
await this.featureLifecycleReadModel.getStageCount();
|
||||
|
||||
const result: Record<string, number> = stageCounts.reduce(
|
||||
(acc, { stage, count }) => {
|
||||
acc[stage === 'pre-live' ? 'preLive' : stage] = count;
|
||||
return acc;
|
||||
},
|
||||
{ initial: 0, preLive: 0, live: 0, completed: 0, archived: 0 },
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
featureLifecycleCountSchema.$id,
|
||||
result,
|
||||
);
|
||||
}
|
||||
}
|
@ -4,25 +4,15 @@ 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';
|
||||
import type { IFlagResolver } from '../../types';
|
||||
|
||||
let db: ITestDb;
|
||||
let featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||
let featureLifecycleStore: IFeatureLifecycleStore;
|
||||
let featureToggleStore: IFeatureToggleStore;
|
||||
|
||||
const alwaysOnFlagResolver = {
|
||||
isEnabled() {
|
||||
return true;
|
||||
},
|
||||
} as unknown as IFlagResolver;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('feature_lifecycle_read_model', getLogger);
|
||||
featureLifecycleReadModel = new FeatureLifecycleReadModel(
|
||||
db.rawDatabase,
|
||||
alwaysOnFlagResolver,
|
||||
);
|
||||
featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
|
||||
featureLifecycleStore = db.stores.featureLifecycleStore;
|
||||
featureToggleStore = db.stores.featureToggleStore;
|
||||
});
|
||||
|
@ -7,7 +7,6 @@ import type {
|
||||
import { getCurrentStage } from './get-current-stage';
|
||||
import type {
|
||||
IFeatureLifecycleStage,
|
||||
IFlagResolver,
|
||||
IProjectLifecycleStageDuration,
|
||||
StageName,
|
||||
} from '../../types';
|
||||
@ -28,11 +27,8 @@ type DBProjectType = DBType & {
|
||||
export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
|
||||
private db: Db;
|
||||
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
constructor(db: Db, flagResolver: IFlagResolver) {
|
||||
constructor(db: Db) {
|
||||
this.db = db;
|
||||
this.flagResolver = flagResolver;
|
||||
}
|
||||
|
||||
async getStageCount(): Promise<StageCount[]> {
|
||||
|
@ -41,10 +41,7 @@ beforeAll(async () => {
|
||||
);
|
||||
eventStore = db.stores.eventStore;
|
||||
eventBus = app.config.eventBus;
|
||||
featureLifecycleReadModel = new FeatureLifecycleReadModel(
|
||||
db.rawDatabase,
|
||||
app.config.flagResolver,
|
||||
);
|
||||
featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
|
||||
featureLifecycleStore = db.stores.featureLifecycleStore;
|
||||
|
||||
await app.request
|
||||
@ -115,6 +112,10 @@ const expectFeatureStage = async (featureName: string, stage: StageName) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getFeaturesLifecycleCount = async () => {
|
||||
return app.request.get(`/api/admin/lifecycle/count`).expect(200);
|
||||
};
|
||||
|
||||
test('should return lifecycle stages', async () => {
|
||||
await app.createFeature('my_feature_a');
|
||||
await app.enableFeature('my_feature_a', 'default');
|
||||
@ -173,6 +174,15 @@ test('should return lifecycle stages', async () => {
|
||||
|
||||
eventStore.emit(FEATURE_REVIVED, { featureName: 'my_feature_a' });
|
||||
await reachedStage('my_feature_a', 'initial');
|
||||
|
||||
const { body: lifecycleCount } = await getFeaturesLifecycleCount();
|
||||
expect(lifecycleCount).toEqual({
|
||||
initial: 1,
|
||||
preLive: 0,
|
||||
live: 0,
|
||||
completed: 0,
|
||||
archived: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('should be able to toggle between completed/uncompleted', async () => {
|
||||
|
@ -122,10 +122,7 @@ export const createFeatureToggleService = (
|
||||
|
||||
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);
|
||||
|
||||
const featureLifecycleReadModel = new FeatureLifecycleReadModel(
|
||||
db,
|
||||
config.flagResolver,
|
||||
);
|
||||
const featureLifecycleReadModel = new FeatureLifecycleReadModel(db);
|
||||
|
||||
const dependentFeaturesService = createDependentFeaturesService(config)(db);
|
||||
|
||||
|
@ -67,10 +67,7 @@ beforeAll(async () => {
|
||||
const versionService = new VersionService(stores, config);
|
||||
db = await dbInit('metrics_test', getLogger);
|
||||
|
||||
featureLifeCycleReadModel = new FeatureLifecycleReadModel(
|
||||
db.rawDatabase,
|
||||
config.flagResolver,
|
||||
);
|
||||
featureLifeCycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
|
||||
stores.featureLifecycleReadModel = featureLifeCycleReadModel;
|
||||
featureLifeCycleStore = new FeatureLifecycleStore(db.rawDatabase);
|
||||
stores.featureLifecycleStore = featureLifeCycleStore;
|
||||
|
43
src/lib/openapi/spec/feature-lifecycle-count-schema.ts
Normal file
43
src/lib/openapi/spec/feature-lifecycle-count-schema.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const featureLifecycleCountSchema = {
|
||||
$id: '#/components/schemas/featureLifecycleCountSchema',
|
||||
type: 'object',
|
||||
description: 'A number features in each of the lifecycle stages',
|
||||
required: ['initial', 'preLive', 'live', 'completed', 'archived'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
initial: {
|
||||
type: 'number',
|
||||
example: 1,
|
||||
description: 'Number of features in the initial stage',
|
||||
},
|
||||
preLive: {
|
||||
type: 'number',
|
||||
example: 1,
|
||||
description: 'Number of features in the pre-live stage',
|
||||
},
|
||||
live: {
|
||||
type: 'number',
|
||||
example: 1,
|
||||
description: 'Number of features in the live stage',
|
||||
},
|
||||
completed: {
|
||||
type: 'number',
|
||||
example: 1,
|
||||
description: 'Number of features in the completed stage',
|
||||
},
|
||||
archived: {
|
||||
type: 'number',
|
||||
example: 1,
|
||||
description: 'Number of features in the archived stage',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type FeatureLifecycleCountSchema = FromSchema<
|
||||
typeof featureLifecycleCountSchema
|
||||
>;
|
@ -83,6 +83,7 @@ export * from './feature-environment-metrics-schema';
|
||||
export * from './feature-environment-schema';
|
||||
export * from './feature-events-schema';
|
||||
export * from './feature-lifecycle-completed-schema';
|
||||
export * from './feature-lifecycle-count-schema';
|
||||
export * from './feature-lifecycle-schema';
|
||||
export * from './feature-metrics-schema';
|
||||
export * from './feature-schema';
|
||||
|
@ -36,6 +36,7 @@ import { InactiveUsersController } from '../../users/inactive/inactive-users-con
|
||||
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller';
|
||||
import { SearchApi } from './search';
|
||||
import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller';
|
||||
import FeatureLifecycleCountController from '../../features/feature-lifecycle/feature-lifecycle-count-controller';
|
||||
|
||||
export class AdminApi extends Controller {
|
||||
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
||||
@ -121,6 +122,10 @@ export class AdminApi extends Controller {
|
||||
'/projects',
|
||||
new ProjectController(config, services, db).router,
|
||||
);
|
||||
this.app.use(
|
||||
'/lifecycle',
|
||||
new FeatureLifecycleCountController(config, services).router,
|
||||
);
|
||||
this.app.use(
|
||||
'/personal-dashboard',
|
||||
new PersonalDashboardController(config, services).router,
|
||||
|
@ -198,7 +198,7 @@ export const createServices = (
|
||||
? new DependentFeaturesReadModel(db)
|
||||
: new FakeDependentFeaturesReadModel();
|
||||
const featureLifecycleReadModel = db
|
||||
? new FeatureLifecycleReadModel(db, config.flagResolver)
|
||||
? new FeatureLifecycleReadModel(db)
|
||||
: new FakeFeatureLifecycleReadModel();
|
||||
|
||||
const transactionalContextService = db
|
||||
@ -490,6 +490,7 @@ export const createServices = (
|
||||
projectStatusService,
|
||||
transactionalUserSubscriptionsService,
|
||||
uniqueConnectionService,
|
||||
featureLifecycleReadModel,
|
||||
};
|
||||
};
|
||||
|
||||
@ -544,4 +545,5 @@ export {
|
||||
ProjectStatusService,
|
||||
UserSubscriptionsService,
|
||||
UniqueConnectionService,
|
||||
FeatureLifecycleReadModel,
|
||||
};
|
||||
|
@ -60,6 +60,7 @@ import type { PersonalDashboardService } from '../features/personal-dashboard/pe
|
||||
import type { ProjectStatusService } from '../features/project-status/project-status-service';
|
||||
import type { UserSubscriptionsService } from '../features/user-subscriptions/user-subscriptions-service';
|
||||
import type { UniqueConnectionService } from '../features/unique-connection/unique-connection-service';
|
||||
import type { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
|
||||
|
||||
export interface IUnleashServices {
|
||||
transactionalAccessService: WithTransactional<AccessService>;
|
||||
@ -131,4 +132,5 @@ export interface IUnleashServices {
|
||||
projectStatusService: ProjectStatusService;
|
||||
transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
|
||||
uniqueConnectionService: UniqueConnectionService;
|
||||
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user