1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-11-24 20:06:55 +01:00

chore: remove release plans from getFeature endpoint (#10955)

This commit is contained in:
Jaanus Sellin 2025-11-10 15:06:21 +02:00 committed by GitHub
parent 529726decf
commit 96118836d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 0 additions and 505 deletions

View File

@ -66,8 +66,6 @@ import {
createFeatureLinkService,
} from '../feature-links/createFeatureLinkService.js';
import { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
import { ReleasePlanReadModel } from '../release-plans/release-plan-read-model.js';
import { FakeReleasePlanReadModel } from '../../../test/fixtures/fake/fake-release-plan-read-model.js';
export const createFeatureToggleService = (
db: Db,
@ -143,8 +141,6 @@ export const createFeatureToggleService = (
const resourceLimitsService = new ResourceLimitsService(config);
const releasePlanReadModel = new ReleasePlanReadModel(db, eventBus);
const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
@ -170,7 +166,6 @@ export const createFeatureToggleService = (
featureLinksReadModel,
featureLinkService,
resourceLimitsService,
releasePlanReadModel,
},
);
return featureToggleService;
@ -216,7 +211,6 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const { featureLinkService } = createFakeFeatureLinkService(config);
const resourceLimitsService = new ResourceLimitsService(config);
const releasePlanReadModel = new FakeReleasePlanReadModel();
const featureToggleService = new FeatureToggleService(
{
@ -247,7 +241,6 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
featureLinksReadModel,
featureLinkService,
resourceLimitsService,
releasePlanReadModel,
},
);
return {

View File

@ -19,7 +19,6 @@ import {
type IAuditUser,
type IConstraint,
type IDependency,
type IEnvironmentDetail,
type IFeatureCollaboratorsReadModel,
type IFeatureEnvironmentInfo,
type IFeatureEnvironmentStore,
@ -118,8 +117,6 @@ import { sortStrategies } from '../../util/sortStrategies.js';
import type FeatureLinkService from '../feature-links/feature-link-service.js';
import type { IFeatureLink } from '../feature-links/feature-links-read-model-type.js';
import type { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
import type { IReleasePlanReadModel } from '../release-plans/release-plan-read-model-type.js';
import type { ReleasePlan } from '../release-plans/release-plan.js';
interface IFeatureContext {
featureName: string;
projectId: string;
@ -180,7 +177,6 @@ export type ServicesAndReadModels = {
featureLinkService: FeatureLinkService;
featureLinksReadModel: IFeatureLinksReadModel;
resourceLimitsService: ResourceLimitsService;
releasePlanReadModel: IReleasePlanReadModel;
};
export class FeatureToggleService {
@ -230,8 +226,6 @@ export class FeatureToggleService {
private resourceLimitsService: ResourceLimitsService;
private releasePlanReadModel: IReleasePlanReadModel;
constructor(
{
featureStrategiesStore,
@ -257,7 +251,6 @@ export class FeatureToggleService {
featureLinksReadModel,
featureLinkService,
resourceLimitsService,
releasePlanReadModel,
}: ServicesAndReadModels,
) {
this.logger = getLogger('services/feature-toggle-service.ts');
@ -283,7 +276,6 @@ export class FeatureToggleService {
this.featureLinkService = featureLinkService;
this.eventBus = eventBus;
this.resourceLimitsService = resourceLimitsService;
this.releasePlanReadModel = releasePlanReadModel;
}
async validateFeaturesContext(
@ -1160,15 +1152,8 @@ export class FeatureToggleService {
archived,
);
const environmentsWithReleasePlans =
await this.addReleasePlansToEnvironments(
featureName,
result.environments,
);
return {
...result,
environments: environmentsWithReleasePlans,
dependencies,
children,
lifecycle,
@ -1187,15 +1172,8 @@ export class FeatureToggleService {
archived,
);
const environmentsWithReleasePlans =
await this.addReleasePlansToEnvironments(
featureName,
result.environments,
);
return {
...result,
environments: environmentsWithReleasePlans,
dependencies,
children,
lifecycle,
@ -1205,27 +1183,6 @@ export class FeatureToggleService {
}
}
private async addReleasePlansToEnvironments(
featureName: string,
environments: IEnvironmentDetail[],
): Promise<(IEnvironmentDetail & { releasePlans?: ReleasePlan[] })[]> {
if (!this.flagResolver.isEnabled('featureReleasePlans')) {
return environments;
}
const environmentNames = environments.map((env) => env.name);
const releasePlansByEnvironment =
await this.releasePlanReadModel.getReleasePlans(
featureName,
environmentNames,
);
return environments.map((env) => ({
...env,
releasePlans: releasePlansByEnvironment[env.name] || [],
}));
}
async getVariantsForEnv(
featureName: string,
environment: string,

View File

@ -1,8 +0,0 @@
import type { ReleasePlan } from './release-plan.js';
export interface IReleasePlanReadModel {
getReleasePlans(
featureName: string,
environments: string[],
): Promise<Record<string, ReleasePlan[]>>;
}

View File

@ -1,216 +0,0 @@
import { ulid } from 'ulidx';
import { EventEmitter } from 'events';
import dbInit, {
type ITestDb,
} from '../../../test/e2e/helpers/database-init.js';
import getLogger from '../../../test/fixtures/no-logger.js';
import { ReleasePlanReadModel } from './release-plan-read-model.js';
import type { IReleasePlanReadModel } from './release-plan-read-model-type.js';
import type { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type.js';
import type { ReleasePlanStore } from './release-plan-store.js';
import type { ReleasePlanMilestoneStore } from './release-plan-milestone-store.js';
import type { IFeatureEnvironmentStore } from '../../types/stores/feature-environment-store.js';
let db: ITestDb;
let releasePlanReadModel: IReleasePlanReadModel;
let featureToggleStore: IFeatureToggleStore;
let releasePlanStore: ReleasePlanStore;
let releasePlanMilestoneStore: ReleasePlanMilestoneStore;
let featureEnvironmentStore: IFeatureEnvironmentStore;
let eventBus: EventEmitter;
beforeAll(async () => {
db = await dbInit('release_plan_read_model', getLogger);
eventBus = new EventEmitter();
releasePlanReadModel = new ReleasePlanReadModel(db.rawDatabase, eventBus);
featureToggleStore = db.stores.featureToggleStore;
releasePlanStore = db.stores.releasePlanStore;
releasePlanMilestoneStore = db.stores.releasePlanMilestoneStore;
featureEnvironmentStore = db.stores.featureEnvironmentStore;
});
afterAll(async () => {
if (db) {
await db.destroy();
}
});
beforeEach(async () => {
await releasePlanStore.deleteAll();
await releasePlanMilestoneStore.deleteAll();
await featureToggleStore.deleteAll();
await featureEnvironmentStore.deleteAll();
await db.rawDatabase('milestone_progressions').del();
});
const createReleasePlan = async (planData: {
name: string;
description: string;
featureName: string;
environment: string;
createdByUserId: number;
activeMilestoneId?: string;
}) => {
const id = ulid();
await db.rawDatabase('release_plan_definitions').insert({
id,
name: planData.name,
description: planData.description,
feature_name: planData.featureName,
environment: planData.environment,
created_by_user_id: planData.createdByUserId,
active_milestone_id: planData.activeMilestoneId || null,
discriminator: 'plan',
created_at: new Date(),
});
return {
id,
name: planData.name,
description: planData.description,
featureName: planData.featureName,
environment: planData.environment,
createdByUserId: planData.createdByUserId,
activeMilestoneId: planData.activeMilestoneId || null,
};
};
const createMilestone = async (milestoneData: {
name: string;
sortOrder: number;
releasePlanDefinitionId: string;
startedAt?: Date;
}) => {
const milestone = await releasePlanMilestoneStore.insert({
name: milestoneData.name,
sortOrder: milestoneData.sortOrder,
releasePlanDefinitionId: milestoneData.releasePlanDefinitionId,
});
if (milestoneData.startedAt) {
await db
.rawDatabase('milestones')
.where('id', milestone.id)
.update('started_at', milestoneData.startedAt);
}
return {
id: milestone.id,
name: milestone.name,
sortOrder: milestone.sortOrder,
releasePlanDefinitionId: milestone.releasePlanDefinitionId,
startedAt: milestoneData.startedAt || null,
};
};
const createMilestoneProgression = async (progressionData: {
sourceMilestoneId: string;
targetMilestoneId: string;
transitionCondition?: object;
executedAt?: Date;
}) => {
await db.rawDatabase('milestone_progressions').insert({
source_milestone: progressionData.sourceMilestoneId,
target_milestone: progressionData.targetMilestoneId,
transition_condition: progressionData.transitionCondition || null,
executed_at: progressionData.executedAt || null,
});
return {
transitionCondition: progressionData.transitionCondition || null,
executedAt: progressionData.executedAt || null,
};
};
test('should return release plans with complete milestone data', async () => {
await featureToggleStore.create('default', {
name: 'test-feature',
createdByUserId: 1,
});
await featureEnvironmentStore.addEnvironmentToFeature(
'test-feature',
'development',
true,
);
const plan = await createReleasePlan({
name: 'Test Plan',
description: 'Test plan',
featureName: 'test-feature',
environment: 'development',
createdByUserId: 1,
});
const startedAt = new Date('2024-01-10T08:00:00.000Z');
const milestone1 = await createMilestone({
name: 'Milestone 1',
sortOrder: 1,
releasePlanDefinitionId: plan.id,
startedAt: startedAt,
});
const milestone2 = await createMilestone({
name: 'Milestone 2',
sortOrder: 2,
releasePlanDefinitionId: plan.id,
});
await releasePlanStore.update(plan.id, {
activeMilestoneId: milestone1.id,
});
const transitionCondition = { intervalMinutes: 60 };
const executedAt = new Date('2024-01-15T10:00:00.000Z');
const milestoneProgression = await createMilestoneProgression({
sourceMilestoneId: milestone1.id,
targetMilestoneId: milestone2.id,
transitionCondition: transitionCondition,
executedAt: executedAt,
});
const releasePlans = await releasePlanReadModel.getReleasePlans(
'test-feature',
['development'],
);
expect(releasePlans).toMatchObject({
development: [
{
id: plan.id,
discriminator: 'plan',
name: plan.name,
description: plan.description,
featureName: plan.featureName,
environment: plan.environment,
createdByUserId: plan.createdByUserId,
createdAt: expect.any(Date),
activeMilestoneId: milestone1.id,
releasePlanTemplateId: null,
milestones: [
{
id: milestone1.id,
name: milestone1.name,
sortOrder: milestone1.sortOrder,
releasePlanDefinitionId:
milestone1.releasePlanDefinitionId,
startedAt: milestone1.startedAt,
progressionExecutedAt: milestoneProgression.executedAt,
transitionCondition:
milestoneProgression.transitionCondition,
strategies: [],
},
{
id: milestone2.id,
name: milestone2.name,
sortOrder: milestone2.sortOrder,
releasePlanDefinitionId:
milestone2.releasePlanDefinitionId,
startedAt: milestone2.startedAt,
progressionExecutedAt: null,
transitionCondition: null,
strategies: [],
},
],
},
],
});
});

View File

@ -1,210 +0,0 @@
import type { Db } from '../../db/db.js';
import type { IReleasePlanReadModel } from './release-plan-read-model-type.js';
import type { ReleasePlan } from './release-plan.js';
import metricsHelper from '../../util/metrics-helper.js';
import type EventEmitter from 'events';
const TABLE = 'release_plan_definitions';
const selectColumns = [
'rpd.id AS planId',
'rpd.discriminator AS planDiscriminator',
'rpd.name AS planName',
'rpd.description as planDescription',
'rpd.feature_name as planFeatureName',
'rpd.environment as planEnvironment',
'rpd.created_by_user_id as planCreatedByUserId',
'rpd.created_at as planCreatedAt',
'rpd.active_milestone_id as planActiveMilestoneId',
'rpd.release_plan_template_id as planTemplateId',
'mi.id AS milestoneId',
'mi.name AS milestoneName',
'mi.sort_order AS milestoneSortOrder',
'mi.started_at AS milestoneStartedAt',
'mp.transition_condition AS milestoneTransitionCondition',
'mp.executed_at AS milestoneProgressionExecutedAt',
'ms.id AS strategyId',
'ms.sort_order AS strategySortOrder',
'ms.title AS strategyTitle',
'ms.strategy_name AS strategyName',
'ms.parameters AS strategyParameters',
'ms.constraints AS strategyConstraints',
'ms.variants AS strategyVariants',
'mss.segment_id AS segmentId',
];
const processReleasePlanRows = (templateRows): ReleasePlan[] =>
templateRows.reduce(
(
acc: ReleasePlan[],
{
planId,
planDiscriminator,
planName,
planDescription,
planFeatureName,
planEnvironment,
planCreatedByUserId,
planCreatedAt,
planActiveMilestoneId,
planTemplateId,
milestoneId,
milestoneName,
milestoneSortOrder,
milestoneStartedAt,
milestoneTransitionCondition,
milestoneProgressionExecutedAt,
strategyId,
strategySortOrder,
strategyTitle,
strategyName,
strategyParameters,
strategyConstraints,
strategyVariants,
segmentId,
},
) => {
let plan = acc.find(({ id }) => id === planId);
if (!plan) {
plan = {
id: planId,
discriminator: planDiscriminator,
name: planName,
description: planDescription,
featureName: planFeatureName,
environment: planEnvironment,
createdByUserId: planCreatedByUserId,
createdAt: planCreatedAt,
activeMilestoneId: planActiveMilestoneId,
releasePlanTemplateId: planTemplateId,
milestones: [],
};
acc.push(plan);
}
if (!milestoneId) {
return acc;
}
let milestone = plan.milestones.find(
({ id }) => id === milestoneId,
);
if (!milestone) {
milestone = {
id: milestoneId,
name: milestoneName,
sortOrder: milestoneSortOrder,
startedAt: milestoneStartedAt,
transitionCondition: milestoneTransitionCondition,
progressionExecutedAt: milestoneProgressionExecutedAt,
strategies: [],
releasePlanDefinitionId: planId,
};
plan.milestones.push(milestone);
}
if (!strategyId) {
return acc;
}
let strategy = milestone.strategies?.find(
({ id }) => id === strategyId,
);
if (!strategy) {
strategy = {
id: strategyId,
milestoneId: milestoneId,
sortOrder: strategySortOrder,
title: strategyTitle,
strategyName: strategyName,
parameters: strategyParameters ?? {},
constraints: strategyConstraints,
variants: strategyVariants ?? [],
segments: [],
};
milestone.strategies = [
...(milestone.strategies || []),
strategy,
];
}
if (segmentId) {
strategy.segments = [...(strategy.segments || []), segmentId];
}
return acc;
},
[],
);
export class ReleasePlanReadModel implements IReleasePlanReadModel {
private db: Db;
private timer: Function;
constructor(db: Db, eventBus: EventEmitter) {
this.db = db;
this.timer = (action: string) =>
metricsHelper.wrapTimer(eventBus, 'db_time', {
store: 'release-plan-read-model',
action,
});
}
async getReleasePlans(
featureName: string,
environments: string[],
): Promise<Record<string, ReleasePlan[]>> {
const endTimer = this.timer('getReleasePlans');
if (environments.length === 0) {
endTimer();
return {};
}
const planRows = await this.db(`${TABLE} AS rpd`)
.where('rpd.discriminator', 'plan')
.andWhere('rpd.feature_name', featureName)
.whereIn('rpd.environment', environments)
.leftJoin(
'milestones AS mi',
'mi.release_plan_definition_id',
'rpd.id',
)
.leftJoin(
'milestone_progressions AS mp',
'mp.source_milestone',
'mi.id',
)
.leftJoin('milestone_strategies AS ms', 'ms.milestone_id', 'mi.id')
.leftJoin(
'milestone_strategy_segments AS mss',
'mss.milestone_strategy_id',
'ms.id',
)
.orderBy('rpd.environment', 'asc')
.orderBy('mi.sort_order', 'asc')
.orderBy('ms.sort_order', 'asc')
.select(selectColumns);
const allPlans = processReleasePlanRows(planRows);
const plansByEnvironment: Record<string, ReleasePlan[]> = {};
for (const plan of allPlans) {
if (!plansByEnvironment[plan.environment]) {
plansByEnvironment[plan.environment] = [];
}
plansByEnvironment[plan.environment].push(plan);
}
for (const env of environments) {
if (!plansByEnvironment[env]) {
plansByEnvironment[env] = [];
}
}
endTimer();
return plansByEnvironment;
}
}

View File

@ -188,8 +188,6 @@ import type { IClientInstance } from './types/stores/client-instance-store.js';
import EnvironmentStore from './features/project-environments/environment-store.js';
import ProjectStore from './features/project/project-store.js';
import type { ReleasePlanMilestoneWriteModel } from './features/release-plans/release-plan-milestone.js';
import type { IReleasePlanReadModel } from './features/release-plans/release-plan-read-model-type.js';
import { ReleasePlanReadModel } from './features/release-plans/release-plan-read-model.js';
import { FakeChangeRequestAccessReadModel } from './features/change-request-access-service/fake-change-request-access-read-model.js';
import { fakeImpactMetricsResolver } from '../test/fixtures/fake-impact-metrics.js';
@ -505,7 +503,6 @@ export {
impactRegister,
EnvironmentStore,
ProjectStore,
ReleasePlanReadModel,
};
export type {
@ -547,7 +544,6 @@ export type {
ReleasePlanMilestoneWriteModel,
ReleasePlanMilestoneStrategyWriteModel,
IChangeRequestAccessReadModel,
IReleasePlanReadModel,
IRoleWithProject,
ISchemaValidationErrors,
IImportService,

View File

@ -1,17 +0,0 @@
import type { IReleasePlanReadModel } from '../../../lib/features/release-plans/release-plan-read-model-type.js';
import type { ReleasePlan } from '../../../lib/features/release-plans/release-plan.js';
export class FakeReleasePlanReadModel implements IReleasePlanReadModel {
private releasePlans: Record<string, ReleasePlan[]>;
constructor(releasePlans: Record<string, ReleasePlan[]> = {}) {
this.releasePlans = releasePlans;
}
async getReleasePlans(
_featureName: string,
_environments: string[],
): Promise<Record<string, ReleasePlan[]>> {
return this.releasePlans;
}
}