1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-04 00:18:40 +01:00
unleash.unleash/src/lib/features/dependent-features/dependent-features-service.ts
2024-03-28 16:53:30 +01:00

257 lines
8.4 KiB
TypeScript

import { InvalidOperationError, PermissionError } from '../../error';
import type { CreateDependentFeatureSchema } from '../../openapi';
import type { IDependentFeaturesStore } from './dependent-features-store-type';
import type {
FeatureDependency,
FeatureDependencyId,
} from './dependent-features';
import type { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
import type { EventService } from '../../services';
import type { IUser } from '../../server-impl';
import { SKIP_CHANGE_REQUEST } from '../../types';
import type { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model';
import { extractUsernameFromUser } from '../../util';
import type { IFeaturesReadModel } from '../feature-toggle/types/features-read-model-type';
interface IDependentFeaturesServiceDeps {
dependentFeaturesStore: IDependentFeaturesStore;
dependentFeaturesReadModel: IDependentFeaturesReadModel;
changeRequestAccessReadModel: IChangeRequestAccessReadModel;
featuresReadModel: IFeaturesReadModel;
eventService: EventService;
}
export class DependentFeaturesService {
private dependentFeaturesStore: IDependentFeaturesStore;
private dependentFeaturesReadModel: IDependentFeaturesReadModel;
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
private featuresReadModel: IFeaturesReadModel;
private eventService: EventService;
constructor({
featuresReadModel,
dependentFeaturesReadModel,
dependentFeaturesStore,
eventService,
changeRequestAccessReadModel,
}: IDependentFeaturesServiceDeps) {
this.dependentFeaturesStore = dependentFeaturesStore;
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.featuresReadModel = featuresReadModel;
this.eventService = eventService;
}
async cloneDependencies(
{
featureName,
newFeatureName,
projectId,
}: { featureName: string; newFeatureName: string; projectId: string },
user: string,
userId: number,
) {
const parents =
await this.dependentFeaturesReadModel.getParents(featureName);
await Promise.all(
parents.map((parent) =>
this.unprotectedUpsertFeatureDependency(
{ child: newFeatureName, projectId },
{
feature: parent.feature,
enabled: parent.enabled,
variants: parent.variants,
},
user,
userId,
),
),
);
}
async upsertFeatureDependency(
{ child, projectId }: { child: string; projectId: string },
dependentFeature: CreateDependentFeatureSchema,
user: IUser,
): Promise<void> {
await this.stopWhenChangeRequestsEnabled(projectId, user);
return this.unprotectedUpsertFeatureDependency(
{ child, projectId },
dependentFeature,
extractUsernameFromUser(user),
user.id,
);
}
async unprotectedUpsertFeatureDependency(
{ child, projectId }: { child: string; projectId: string },
dependentFeature: CreateDependentFeatureSchema,
user: string,
userId: number,
): Promise<void> {
const { enabled, feature: parent, variants } = dependentFeature;
if (child === parent) {
throw new InvalidOperationError(
'A feature flag cannot depend on itself.',
);
}
const [grandchildren, grandparents, parentExists, sameProject] =
await Promise.all([
this.dependentFeaturesReadModel.getChildren([child]),
this.dependentFeaturesReadModel.getParents(parent),
this.featuresReadModel.featureExists(parent),
this.featuresReadModel.featuresInTheSameProject(child, parent),
]);
if (grandchildren.length > 0) {
throw new InvalidOperationError(
'Transitive dependency detected. Cannot add a dependency to the feature that other features depend on.',
);
}
if (grandparents.length > 0) {
throw new InvalidOperationError(
'Transitive dependency detected. Cannot add a dependency to the feature that has parent dependency.',
);
}
if (!parentExists) {
throw new InvalidOperationError(
`No active feature ${parent} exists`,
);
}
if (!sameProject) {
throw new InvalidOperationError(
'Parent and child features should be in the same project',
);
}
const featureDependency: FeatureDependency =
enabled === false
? {
parent,
child,
enabled,
}
: {
parent,
child,
enabled: true,
variants,
};
await this.dependentFeaturesStore.upsert(featureDependency);
await this.eventService.storeEvent({
type: 'feature-dependency-added',
project: projectId,
featureName: child,
createdBy: user,
createdByUserId: userId,
data: {
feature: parent,
enabled: featureDependency.enabled,
...(variants !== undefined && { variants }),
},
});
}
async deleteFeatureDependency(
dependency: FeatureDependencyId,
projectId: string,
user: IUser,
): Promise<void> {
await this.stopWhenChangeRequestsEnabled(projectId, user);
return this.unprotectedDeleteFeatureDependency(
dependency,
projectId,
extractUsernameFromUser(user),
user.id,
);
}
async unprotectedDeleteFeatureDependency(
dependency: FeatureDependencyId,
projectId: string,
user: string,
userId: number,
): Promise<void> {
await this.dependentFeaturesStore.delete(dependency);
await this.eventService.storeEvent({
type: 'feature-dependency-removed',
project: projectId,
featureName: dependency.child,
createdBy: user,
createdByUserId: userId,
data: { feature: dependency.parent },
});
}
async deleteFeaturesDependencies(
features: string[],
projectId: string,
user: IUser,
): Promise<void> {
await this.stopWhenChangeRequestsEnabled(projectId, user);
return this.unprotectedDeleteFeaturesDependencies(
features,
projectId,
extractUsernameFromUser(user),
user.id,
);
}
async unprotectedDeleteFeaturesDependencies(
features: string[],
projectId: string,
user: string,
userId: number,
): Promise<void> {
await this.dependentFeaturesStore.deleteAll(features);
await this.eventService.storeEvents(
features.map((feature) => ({
type: 'feature-dependencies-removed',
project: projectId,
featureName: feature,
createdBy: user,
createdByUserId: userId,
})),
);
}
async getPossibleParentFeatures(feature: string): Promise<string[]> {
return this.dependentFeaturesReadModel.getPossibleParentFeatures(
feature,
);
}
async getPossibleParentVariants(parentFeature: string): Promise<string[]> {
return this.dependentFeaturesReadModel.getPossibleParentVariants(
parentFeature,
);
}
async checkDependenciesExist(): Promise<boolean> {
return this.dependentFeaturesReadModel.hasAnyDependencies();
}
private async stopWhenChangeRequestsEnabled(project: string, user?: IUser) {
const canBypass =
await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(
project,
user,
);
if (!canBypass) {
throw new PermissionError(SKIP_CHANGE_REQUEST);
}
}
}