UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

115 lines 5.75 kB
import { InvalidOperationError, PermissionError } from '../../error/index.js'; import { FeatureDependenciesRemovedEvent, FeatureDependencyAddedEvent, FeatureDependencyRemovedEvent, SKIP_CHANGE_REQUEST, } from '../../types/index.js'; export class DependentFeaturesService { constructor({ featuresReadModel, dependentFeaturesReadModel, dependentFeaturesStore, eventService, changeRequestAccessReadModel, }) { this.dependentFeaturesStore = dependentFeaturesStore; this.dependentFeaturesReadModel = dependentFeaturesReadModel; this.changeRequestAccessReadModel = changeRequestAccessReadModel; this.featuresReadModel = featuresReadModel; this.eventService = eventService; } async cloneDependencies({ featureName, newFeatureName, projectId, }, auditUser) { 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, }, auditUser))); } async upsertFeatureDependency({ child, projectId }, dependentFeature, user, auditUser) { await this.stopWhenChangeRequestsEnabled(projectId, user); return this.unprotectedUpsertFeatureDependency({ child, projectId }, dependentFeature, auditUser); } async unprotectedUpsertFeatureDependency({ child, projectId }, dependentFeature, auditUser) { 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 = enabled === false ? { parent, child, enabled, } : { parent, child, enabled: true, variants, }; await this.dependentFeaturesStore.upsert(featureDependency); await this.eventService.storeEvent(new FeatureDependencyAddedEvent({ project: projectId, featureName: child, auditUser, data: { feature: parent, enabled: featureDependency.enabled, ...(variants !== undefined && { variants }), }, })); } async deleteFeatureDependency(dependency, projectId, user, auditUser) { await this.stopWhenChangeRequestsEnabled(projectId, user); return this.unprotectedDeleteFeatureDependency(dependency, projectId, auditUser); } async unprotectedDeleteFeatureDependency(dependency, projectId, auditUser) { await this.dependentFeaturesStore.delete(dependency); await this.eventService.storeEvent(new FeatureDependencyRemovedEvent({ project: projectId, featureName: dependency.child, auditUser, data: { feature: dependency.parent }, })); } async deleteFeaturesDependencies(features, projectId, user, auditUser) { await this.stopWhenChangeRequestsEnabled(projectId, user); return this.unprotectedDeleteFeaturesDependencies(features, projectId, auditUser); } async unprotectedDeleteFeaturesDependencies(features, projectId, auditUser) { const dependencies = await this.dependentFeaturesReadModel.getDependencies(features); const featuresWithDependencies = dependencies.map((dependency) => dependency.feature); if (featuresWithDependencies.length > 0) { await this.dependentFeaturesStore.deleteAll(featuresWithDependencies); await this.eventService.storeEvents(featuresWithDependencies.map((feature) => new FeatureDependenciesRemovedEvent({ project: projectId, featureName: feature, auditUser, }))); } } async getPossibleParentFeatures(feature) { return this.dependentFeaturesReadModel.getPossibleParentFeatures(feature); } async getPossibleParentVariants(parentFeature) { return this.dependentFeaturesReadModel.getPossibleParentVariants(parentFeature); } async checkDependenciesExist() { return this.dependentFeaturesReadModel.hasAnyDependencies(); } async stopWhenChangeRequestsEnabled(project, user) { const canBypass = await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(project, user); if (!canBypass) { throw new PermissionError(SKIP_CHANGE_REQUEST); } } } //# sourceMappingURL=dependent-features-service.js.map