1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-11-01 19:07:38 +01:00
unleash.unleash/src/lib/services/feature-toggle-service.ts

2219 lines
70 KiB
TypeScript

import {
CREATE_FEATURE_STRATEGY,
EnvironmentVariantEvent,
FEATURE_UPDATED,
FeatureArchivedEvent,
FeatureChangeProjectEvent,
FeatureCreatedEvent,
FeatureDeletedEvent,
FeatureEnvironmentEvent,
FeatureMetadataUpdateEvent,
FeatureRevivedEvent,
FeatureStaleEvent,
FeatureStrategyAddEvent,
FeatureStrategyRemoveEvent,
FeatureStrategyUpdateEvent,
FeatureToggle,
FeatureToggleDTO,
FeatureToggleLegacy,
FeatureToggleWithEnvironment,
FeatureVariantEvent,
IConstraint,
IEventStore,
IFeatureEnvironmentInfo,
IFeatureEnvironmentStore,
IFeatureNaming,
IFeatureOverview,
IFeatureStrategy,
IFeatureTagStore,
IFeatureToggleClientStore,
IFeatureToggleQuery,
IFeatureToggleStore,
IFlagResolver,
IProjectStore,
ISegment,
IStrategyConfig,
IStrategyStore,
IUnleashConfig,
IUnleashStores,
IVariant,
PotentiallyStaleOnEvent,
Saved,
SKIP_CHANGE_REQUEST,
StrategiesOrderChangedEvent,
StrategyIds,
Unsaved,
WeightType,
} from '../types';
import { Logger } from '../logger';
import {
ForbiddenError,
FOREIGN_KEY_VIOLATION,
OperationDeniedError,
PatternError,
PermissionError,
} from '../error';
import BadDataError from '../error/bad-data-error';
import NameExistsError from '../error/name-exists-error';
import InvalidOperationError from '../error/invalid-operation-error';
import {
constraintSchema,
featureMetadataSchema,
nameSchema,
variantsArraySchema,
} from '../schema/feature-schema';
import NotFoundError from '../error/notfound-error';
import {
FeatureConfigurationClient,
IFeatureStrategiesStore,
} from '../types/stores/feature-strategies-store';
import {
DATE_OPERATORS,
DEFAULT_ENV,
NUM_OPERATORS,
SEMVER_OPERATORS,
STRING_OPERATORS,
} from '../util';
import { applyPatch, deepClone, Operation } from 'fast-json-patch';
import {
validateDate,
validateLegalValues,
validateNumber,
validateSemver,
validateString,
} from '../util/validators/constraint-types';
import { IContextFieldStore } from 'lib/types/stores/context-field-store';
import { SetStrategySortOrderSchema } from 'lib/openapi/spec/set-strategy-sort-order-schema';
import {
getDefaultStrategy,
getProjectDefaultStrategy,
} from '../features/playground/feature-evaluator/helpers';
import { AccessService } from './access-service';
import { User } from '../server-impl';
import { IFeatureProjectUserParams } from '../routes/admin-api/project/project-features';
import { unique } from '../util/unique';
import { ISegmentService } from 'lib/segments/segment-service-interface';
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';
import { checkFeatureFlagNamesAgainstPattern } from '../features/feature-naming-pattern/feature-naming-validation';
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
interface IFeatureContext {
featureName: string;
projectId: string;
}
interface IFeatureStrategyContext extends IFeatureContext {
environment: string;
}
export interface IGetFeatureParams {
featureName: string;
archived?: boolean;
projectId?: string;
environmentVariants?: boolean;
userId?: number;
}
export type FeatureNameCheckResultWithFeaturePattern =
| { state: 'valid' }
| {
state: 'invalid';
invalidNames: Set<string>;
featureNaming: IFeatureNaming;
};
const oneOf = (values: string[], match: string) => {
return values.some((value) => value === match);
};
class FeatureToggleService {
private logger: Logger;
private featureStrategiesStore: IFeatureStrategiesStore;
private strategyStore: IStrategyStore;
private featureToggleStore: IFeatureToggleStore;
private featureToggleClientStore: IFeatureToggleClientStore;
private tagStore: IFeatureTagStore;
private featureEnvironmentStore: IFeatureEnvironmentStore;
private projectStore: IProjectStore;
private eventStore: IEventStore;
private contextFieldStore: IContextFieldStore;
private segmentService: ISegmentService;
private accessService: AccessService;
private flagResolver: IFlagResolver;
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
private privateProjectChecker: IPrivateProjectChecker;
constructor(
{
featureStrategiesStore,
featureToggleStore,
featureToggleClientStore,
projectStore,
eventStore,
featureTagStore,
featureEnvironmentStore,
contextFieldStore,
strategyStore,
}: Pick<
IUnleashStores,
| 'featureStrategiesStore'
| 'featureToggleStore'
| 'featureToggleClientStore'
| 'projectStore'
| 'eventStore'
| 'featureTagStore'
| 'featureEnvironmentStore'
| 'contextFieldStore'
| 'strategyStore'
>,
{
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
segmentService: ISegmentService,
accessService: AccessService,
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
privateProjectChecker: IPrivateProjectChecker,
) {
this.logger = getLogger('services/feature-toggle-service.ts');
this.featureStrategiesStore = featureStrategiesStore;
this.strategyStore = strategyStore;
this.featureToggleStore = featureToggleStore;
this.featureToggleClientStore = featureToggleClientStore;
this.tagStore = featureTagStore;
this.projectStore = projectStore;
this.eventStore = eventStore;
this.featureEnvironmentStore = featureEnvironmentStore;
this.contextFieldStore = contextFieldStore;
this.segmentService = segmentService;
this.accessService = accessService;
this.flagResolver = flagResolver;
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.privateProjectChecker = privateProjectChecker;
}
async validateFeaturesContext(
featureNames: string[],
projectId: string,
): Promise<void> {
const features = await this.featureToggleStore.getAllByNames(
featureNames,
);
const invalidProjects = unique(
features
.map((feature) => feature.project)
.filter((project) => project !== projectId),
);
if (invalidProjects.length > 0) {
throw new InvalidOperationError(
`The operation could not be completed. The features exist, but the provided project ids ("${invalidProjects.join(
',',
)}") does not match the project provided in request URL ("${projectId}").`,
);
}
}
async validateFeatureBelongsToProject({
featureName,
projectId,
}: IFeatureContext): Promise<void> {
const id = await this.featureToggleStore.getProjectId(featureName);
if (id !== projectId) {
throw new NotFoundError(
`There's no feature named "${featureName}" in project "${projectId}"${
id === undefined
? '.'
: `, but there's a feature with that name in project "${id}"`
}`,
);
}
}
validateUpdatedProperties(
{ featureName, projectId }: IFeatureContext,
existingStrategy: IFeatureStrategy,
): void {
if (existingStrategy.projectId !== projectId) {
throw new InvalidOperationError(
'You can not change the projectId for an activation strategy.',
);
}
if (existingStrategy.featureName !== featureName) {
throw new InvalidOperationError(
'You can not change the featureName for an activation strategy.',
);
}
if (
existingStrategy.parameters &&
'stickiness' in existingStrategy.parameters &&
existingStrategy.parameters.stickiness === ''
) {
throw new InvalidOperationError(
'You can not have an empty string for stickiness.',
);
}
}
async validateProjectCanAccessSegments(
projectId: string,
segmentIds?: number[],
): Promise<void> {
if (segmentIds && segmentIds.length > 0) {
await Promise.all(
segmentIds.map((segmentId) =>
this.segmentService.get(segmentId),
),
).then((segments) =>
segments.map((segment) => {
if (segment.project && segment.project !== projectId) {
throw new BadDataError(
`The segment "${segment.name}" with id ${segment.id} does not belong to project "${projectId}".`,
);
}
}),
);
}
}
async validateStrategyType(
strategyName: string | undefined,
): Promise<void> {
if (strategyName !== undefined) {
const exists = await this.strategyStore.exists(strategyName);
if (!exists) {
throw new BadDataError(
`Could not find strategy type with name ${strategyName}`,
);
}
}
}
async validateConstraints(
constraints: IConstraint[],
): Promise<IConstraint[]> {
const validations = constraints.map((constraint) => {
return this.validateConstraint(constraint);
});
return Promise.all(validations);
}
async validateConstraint(input: IConstraint): Promise<IConstraint> {
const constraint = await constraintSchema.validateAsync(input);
const { operator } = constraint;
const contextDefinition = await this.contextFieldStore.get(
constraint.contextName,
);
if (oneOf(NUM_OPERATORS, operator)) {
await validateNumber(constraint.value);
}
if (oneOf(STRING_OPERATORS, operator)) {
await validateString(constraint.values);
}
if (oneOf(SEMVER_OPERATORS, operator)) {
// Semver library is not asynchronous, so we do not
// need to await here.
validateSemver(constraint.value);
}
if (oneOf(DATE_OPERATORS, operator)) {
await validateDate(constraint.value);
}
if (
contextDefinition &&
contextDefinition.legalValues &&
contextDefinition.legalValues.length > 0
) {
const valuesToValidate = oneOf(
[...DATE_OPERATORS, ...SEMVER_OPERATORS, ...NUM_OPERATORS],
operator,
)
? constraint.value
: constraint.values;
validateLegalValues(
contextDefinition.legalValues,
valuesToValidate,
);
}
return constraint;
}
async patchFeature(
project: string,
featureName: string,
createdBy: string,
operations: Operation[],
): Promise<FeatureToggle> {
const featureToggle = await this.getFeatureMetadata(featureName);
if (operations.some((op) => op.path.indexOf('/variants') >= 0)) {
throw new OperationDeniedError(
`Changing variants is done via PATCH operation to /api/admin/projects/:project/features/:feature/variants`,
);
}
const { newDocument } = applyPatch(
deepClone(featureToggle),
operations,
);
const updated = await this.updateFeatureToggle(
project,
newDocument,
createdBy,
featureName,
);
if (featureToggle.stale !== newDocument.stale) {
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureStaleEvent({
stale: newDocument.stale,
project,
featureName,
createdBy,
tags,
}),
);
}
return updated;
}
featureStrategyToPublic(
featureStrategy: IFeatureStrategy,
segments: ISegment[] = [],
): Saved<IStrategyConfig> {
const result: Saved<IStrategyConfig> = {
id: featureStrategy.id,
name: featureStrategy.strategyName,
title: featureStrategy.title,
disabled: featureStrategy.disabled,
constraints: featureStrategy.constraints || [],
parameters: featureStrategy.parameters,
variants: featureStrategy.variants || [],
sortOrder: featureStrategy.sortOrder,
segments: segments.map((segment) => segment.id) ?? [],
};
return result;
}
async updateStrategiesSortOrder(
context: IFeatureStrategyContext,
sortOrders: SetStrategySortOrderSchema,
createdBy: string,
user?: User,
): Promise<Saved<any>> {
await this.stopWhenChangeRequestsEnabled(
context.projectId,
context.environment,
user,
);
return this.unprotectedUpdateStrategiesSortOrder(
context,
sortOrders,
createdBy,
);
}
async unprotectedUpdateStrategiesSortOrder(
context: IFeatureStrategyContext,
sortOrders: SetStrategySortOrderSchema,
createdBy: string,
): Promise<Saved<any>> {
const { featureName, environment, projectId: project } = context;
const existingOrder = (
await this.getStrategiesForEnvironment(
project,
featureName,
environment,
)
)
.sort((strategy1, strategy2) => {
if (
typeof strategy1.sortOrder === 'number' &&
typeof strategy2.sortOrder === 'number'
) {
return strategy1.sortOrder - strategy2.sortOrder;
}
return 0;
})
.map((strategy) => strategy.id);
const eventPreData: StrategyIds = { strategyIds: existingOrder };
await Promise.all(
sortOrders.map(async ({ id, sortOrder }) => {
await this.featureStrategiesStore.updateSortOrder(
id,
sortOrder,
);
}),
);
const newOrder = (
await this.getStrategiesForEnvironment(
project,
featureName,
environment,
)
)
.sort((strategy1, strategy2) => {
if (
typeof strategy1.sortOrder === 'number' &&
typeof strategy2.sortOrder === 'number'
) {
return strategy1.sortOrder - strategy2.sortOrder;
}
return 0;
})
.map((strategy) => strategy.id);
const eventData: StrategyIds = { strategyIds: newOrder };
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const event = new StrategiesOrderChangedEvent({
featureName,
environment,
project,
createdBy,
preData: eventPreData,
data: eventData,
tags: tags,
});
await this.eventStore.store(event);
}
async createStrategy(
strategyConfig: Unsaved<IStrategyConfig>,
context: IFeatureStrategyContext,
createdBy: string,
user?: User,
): Promise<Saved<IStrategyConfig>> {
await this.stopWhenChangeRequestsEnabled(
context.projectId,
context.environment,
user,
);
return this.unprotectedCreateStrategy(
strategyConfig,
context,
createdBy,
);
}
async unprotectedCreateStrategy(
strategyConfig: Unsaved<IStrategyConfig>,
context: IFeatureStrategyContext,
createdBy: string,
): Promise<Saved<IStrategyConfig>> {
const { featureName, projectId, environment } = context;
await this.validateFeatureBelongsToProject(context);
await this.validateStrategyType(strategyConfig.name);
await this.validateProjectCanAccessSegments(
projectId,
strategyConfig.segments,
);
if (
strategyConfig.constraints &&
strategyConfig.constraints.length > 0
) {
strategyConfig.constraints = await this.validateConstraints(
strategyConfig.constraints,
);
}
if (
strategyConfig.parameters &&
'stickiness' in strategyConfig.parameters &&
strategyConfig.parameters.stickiness === ''
) {
strategyConfig.parameters.stickiness = 'default';
}
if (strategyConfig.variants && strategyConfig.variants.length > 0) {
await variantsArraySchema.validateAsync(strategyConfig.variants);
const fixedVariants = this.fixVariantWeights(
strategyConfig.variants,
);
strategyConfig.variants = fixedVariants;
}
try {
const newFeatureStrategy =
await this.featureStrategiesStore.createStrategyFeatureEnv({
strategyName: strategyConfig.name,
title: strategyConfig.title,
disabled: strategyConfig.disabled,
constraints: strategyConfig.constraints || [],
variants: strategyConfig.variants || [],
parameters: strategyConfig.parameters || {},
sortOrder: strategyConfig.sortOrder,
projectId,
featureName,
environment,
});
if (
strategyConfig.segments &&
Array.isArray(strategyConfig.segments)
) {
await this.segmentService.updateStrategySegments(
newFeatureStrategy.id,
strategyConfig.segments,
);
}
const segments = await this.segmentService.getByStrategy(
newFeatureStrategy.id,
);
const strategy = this.featureStrategyToPublic(
newFeatureStrategy,
segments,
);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureStrategyAddEvent({
project: projectId,
featureName,
createdBy,
environment,
data: strategy,
tags,
}),
);
return strategy;
} catch (e) {
if (e.code === FOREIGN_KEY_VIOLATION) {
throw new BadDataError(
'You have not added the current environment to the project',
);
}
throw e;
}
}
/**
* PUT /api/admin/projects/:projectId/features/:featureName/strategies/:strategyId ?
* {
*
* }
* @param id
* @param updates
* @param context - Which context does this strategy live in (projectId, featureName, environment)
* @param userName - Human readable id of the user performing the update
* @param user - Optional User object performing the action
*/
async updateStrategy(
id: string,
updates: Partial<IStrategyConfig>,
context: IFeatureStrategyContext,
userName: string,
user?: User,
): Promise<Saved<IStrategyConfig>> {
await this.stopWhenChangeRequestsEnabled(
context.projectId,
context.environment,
user,
);
return this.unprotectedUpdateStrategy(id, updates, context, userName);
}
async optionallyDisableFeature(
featureName: string,
environment: string,
projectId: string,
userName: string,
): Promise<void> {
const feature = await this.getFeature({ featureName });
const env = feature.environments.find((e) => e.name === environment);
const hasOnlyDisabledStrategies = env?.strategies.every(
(strategy) => strategy.disabled,
);
if (hasOnlyDisabledStrategies) {
await this.unprotectedUpdateEnabled(
projectId,
featureName,
environment,
false,
userName,
);
}
}
async unprotectedUpdateStrategy(
id: string,
updates: Partial<IStrategyConfig>,
context: IFeatureStrategyContext,
userName: string,
): Promise<Saved<IStrategyConfig>> {
const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id);
this.validateUpdatedProperties(context, existingStrategy);
await this.validateStrategyType(updates.name);
await this.validateProjectCanAccessSegments(
projectId,
updates.segments,
);
if (existingStrategy.id === id) {
if (updates.constraints && updates.constraints.length > 0) {
updates.constraints = await this.validateConstraints(
updates.constraints,
);
}
if (updates.variants && updates.variants.length > 0) {
await variantsArraySchema.validateAsync(updates.variants);
const fixedVariants = this.fixVariantWeights(updates.variants);
updates.variants = fixedVariants;
}
const strategy = await this.featureStrategiesStore.updateStrategy(
id,
updates,
);
if (updates.segments && Array.isArray(updates.segments)) {
await this.segmentService.updateStrategySegments(
strategy.id,
updates.segments,
);
}
const segments = await this.segmentService.getByStrategy(
strategy.id,
);
// Store event!
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const data = this.featureStrategyToPublic(strategy, segments);
const preData = this.featureStrategyToPublic(
existingStrategy,
segments,
);
await this.eventStore.store(
new FeatureStrategyUpdateEvent({
project: projectId,
featureName,
environment,
createdBy: userName,
data,
preData,
tags,
}),
);
await this.optionallyDisableFeature(
featureName,
environment,
projectId,
userName,
);
return data;
}
throw new NotFoundError(`Could not find strategy with id ${id}`);
}
async updateStrategyParameter(
id: string,
name: string,
value: string | number,
context: IFeatureStrategyContext,
userName: string,
): Promise<Saved<IStrategyConfig>> {
const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id);
this.validateUpdatedProperties(context, existingStrategy);
if (existingStrategy.id === id) {
existingStrategy.parameters[name] = String(value);
const strategy = await this.featureStrategiesStore.updateStrategy(
id,
existingStrategy,
);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const segments = await this.segmentService.getByStrategy(
strategy.id,
);
const data = this.featureStrategyToPublic(strategy, segments);
const preData = this.featureStrategyToPublic(
existingStrategy,
segments,
);
await this.eventStore.store(
new FeatureStrategyUpdateEvent({
featureName,
project: projectId,
environment,
createdBy: userName,
data,
preData,
tags,
}),
);
return data;
}
throw new NotFoundError(`Could not find strategy with id ${id}`);
}
/**
* DELETE /api/admin/projects/:projectId/features/:featureName/environments/:environmentName/strategies/:strategyId
* {
*
* }
* @param id - strategy id
* @param context - Which context does this strategy live in (projectId, featureName, environment)
* @param createdBy - Which user does this strategy belong to
* @param user
*/
async deleteStrategy(
id: string,
context: IFeatureStrategyContext,
createdBy: string,
user?: User,
): Promise<void> {
await this.stopWhenChangeRequestsEnabled(
context.projectId,
context.environment,
user,
);
return this.unprotectedDeleteStrategy(id, context, createdBy);
}
async unprotectedDeleteStrategy(
id: string,
context: IFeatureStrategyContext,
createdBy: string,
): Promise<void> {
const existingStrategy = await this.featureStrategiesStore.get(id);
const { featureName, projectId, environment } = context;
this.validateUpdatedProperties(context, existingStrategy);
await this.featureStrategiesStore.delete(id);
const featureStrategies =
await this.featureStrategiesStore.getStrategiesForFeatureEnv(
projectId,
featureName,
environment,
);
const hasOnlyDisabledStrategies = featureStrategies.every(
(strategy) => strategy.disabled,
);
if (hasOnlyDisabledStrategies) {
// Disable the feature in the environment if it only has disabled strategies
await this.unprotectedUpdateEnabled(
projectId,
featureName,
environment,
false,
createdBy,
);
}
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const preData = this.featureStrategyToPublic(existingStrategy);
await this.eventStore.store(
new FeatureStrategyRemoveEvent({
featureName,
project: projectId,
environment,
createdBy,
preData,
tags,
}),
);
// If there are no strategies left for environment disable it
await this.featureEnvironmentStore.disableEnvironmentIfNoStrategies(
featureName,
environment,
);
}
async getStrategiesForEnvironment(
project: string,
featureName: string,
environment: string = DEFAULT_ENV,
): Promise<Saved<IStrategyConfig>[]> {
this.logger.debug('getStrategiesForEnvironment');
const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment(
environment,
featureName,
);
if (hasEnv) {
const featureStrategies =
await this.featureStrategiesStore.getStrategiesForFeatureEnv(
project,
featureName,
environment,
);
const result: Saved<IStrategyConfig>[] = [];
for (const strat of featureStrategies) {
const segments =
(await this.segmentService.getByStrategy(strat.id)).map(
(segment) => segment.id,
) ?? [];
result.push({
id: strat.id,
name: strat.strategyName,
constraints: strat.constraints,
parameters: strat.parameters,
variants: strat.variants,
title: strat.title,
disabled: strat.disabled,
sortOrder: strat.sortOrder,
segments,
});
}
return result;
}
throw new NotFoundError(
`Feature ${featureName} does not have environment ${environment}`,
);
}
/**
* GET /api/admin/projects/:project/features/:featureName
* @param featureName
* @param archived - return archived or non archived toggles
* @param projectId - provide if you're requesting the feature in the context of a specific project.
* @param userId
*/
async getFeature({
featureName,
archived,
projectId,
environmentVariants,
userId,
}: IGetFeatureParams): Promise<FeatureToggleWithEnvironment> {
if (projectId) {
await this.validateFeatureBelongsToProject({
featureName,
projectId,
});
}
if (environmentVariants) {
return this.featureStrategiesStore.getFeatureToggleWithVariantEnvs(
featureName,
userId,
archived,
);
} else {
return this.featureStrategiesStore.getFeatureToggleWithEnvs(
featureName,
userId,
archived,
);
}
}
/**
* GET /api/admin/projects/:project/features/:featureName/variants
* @deprecated - Variants should be fetched from FeatureEnvironmentStore (since variants are now; since 4.18, connected to environments)
* @param featureName
* @return The list of variants
*/
async getVariants(featureName: string): Promise<IVariant[]> {
return this.featureToggleStore.getVariants(featureName);
}
async getVariantsForEnv(
featureName: string,
environment: string,
): Promise<IVariant[]> {
const featureEnvironment = await this.featureEnvironmentStore.get({
featureName,
environment,
});
return featureEnvironment.variants || [];
}
async getFeatureMetadata(featureName: string): Promise<FeatureToggle> {
return this.featureToggleStore.get(featureName);
}
async getClientFeatures(
query?: IFeatureToggleQuery,
): Promise<FeatureConfigurationClient[]> {
const result = await this.featureToggleClientStore.getClient(
query || {},
);
return result.map(
({
name,
type,
enabled,
project,
stale,
strategies,
variants,
description,
impressionData,
dependencies,
}) => ({
name,
type,
enabled,
project,
stale,
strategies,
variants,
description,
impressionData,
dependencies,
}),
);
}
async getPlaygroundFeatures(
query?: IFeatureToggleQuery,
): Promise<FeatureConfigurationClient[]> {
const result = await this.featureToggleClientStore.getPlayground(
query || {},
);
return result;
}
/**
* @deprecated Legacy!
*
* Used to retrieve metadata of all feature toggles defined in Unleash.
* @param query - Allow you to limit search based on criteria such as project, tags, namePrefix. See @IFeatureToggleQuery
* @param archived - Return archived or active toggles
* @returns
*/
async getFeatureToggles(
query?: IFeatureToggleQuery,
userId?: number,
archived: boolean = false,
): Promise<FeatureToggle[]> {
const features = await this.featureToggleClientStore.getAdmin({
featureQuery: query,
userId,
archived,
});
if (this.flagResolver.isEnabled('privateProjects') && userId) {
const projectAccess =
await this.privateProjectChecker.getUserAccessibleProjects(
userId,
);
return projectAccess.mode === 'all'
? features
: features.filter((f) =>
projectAccess.projects.includes(f.project),
);
}
return features;
}
async getFeatureOverview(
params: IFeatureProjectUserParams,
): Promise<IFeatureOverview[]> {
return this.featureStrategiesStore.getFeatureOverview(params);
}
async getFeatureToggle(
featureName: string,
): Promise<FeatureToggleWithEnvironment> {
return this.featureStrategiesStore.getFeatureToggleWithEnvs(
featureName,
);
}
async createFeatureToggle(
projectId: string,
value: FeatureToggleDTO,
createdBy: string,
isValidated: boolean = false,
): Promise<FeatureToggle> {
this.logger.info(`${createdBy} creates feature toggle ${value.name}`);
await this.validateName(value.name);
await this.validateFeatureFlagNameAgainstPattern(value.name, projectId);
const exists = await this.projectStore.hasProject(projectId);
if (await this.projectStore.isFeatureLimitReached(projectId)) {
throw new InvalidOperationError(
'You have reached the maximum number of feature toggles for this project.',
);
}
if (exists) {
let featureData;
if (isValidated) {
featureData = value;
} else {
featureData = await featureMetadataSchema.validateAsync(value);
}
const featureName = featureData.name;
const createdToggle = await this.featureToggleStore.create(
projectId,
featureData,
);
await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject(
featureName,
projectId,
);
if (value.variants && value.variants.length > 0) {
const environments =
await this.featureEnvironmentStore.getEnvironmentsForFeature(
featureName,
);
await this.featureEnvironmentStore.setVariantsToFeatureEnvironments(
featureName,
environments.map((env) => env.environment),
value.variants,
);
}
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureCreatedEvent({
featureName,
createdBy,
project: projectId,
data: createdToggle,
tags,
}),
);
return createdToggle;
}
throw new NotFoundError(`Project with id ${projectId} does not exist`);
}
async checkFeatureFlagNamesAgainstProjectPattern(
projectId: string,
featureNames: string[],
): Promise<FeatureNameCheckResultWithFeaturePattern> {
if (this.flagResolver.isEnabled('featureNamingPattern')) {
const project = await this.projectStore.get(projectId);
const patternData = project.featureNaming;
const namingPattern = patternData?.pattern;
if (namingPattern) {
const result = checkFeatureFlagNamesAgainstPattern(
featureNames,
namingPattern,
);
if (result.state === 'invalid') {
return { ...result, featureNaming: patternData };
}
}
}
return { state: 'valid' };
}
async validateFeatureFlagNameAgainstPattern(
featureName: string,
projectId?: string,
): Promise<void> {
if (projectId) {
const result =
await this.checkFeatureFlagNamesAgainstProjectPattern(
projectId,
[featureName],
);
if (result.state === 'invalid') {
const namingPattern = result.featureNaming.pattern;
const namingExample = result.featureNaming.example;
const namingDescription = result.featureNaming.description;
const error = `The feature flag name "${featureName}" does not match the project's naming pattern: "${namingPattern}".`;
const example = namingExample
? ` Here's an example of a name that does match the pattern: "${namingExample}"."`
: '';
const description = namingDescription
? ` The pattern's description is: "${namingDescription}"`
: '';
throw new PatternError(`${error}${example}${description}`, [
`The flag name does not match the pattern.`,
]);
}
}
}
async cloneFeatureToggle(
featureName: string,
projectId: string,
newFeatureName: string,
replaceGroupId: boolean = true, // eslint-disable-line
userName: string,
): Promise<FeatureToggle> {
const changeRequestEnabled =
await this.changeRequestAccessReadModel.isChangeRequestsEnabledForProject(
projectId,
);
if (changeRequestEnabled) {
throw new ForbiddenError(
`Cloning not allowed. Project ${projectId} has change requests enabled.`,
);
}
this.logger.info(
`${userName} clones feature toggle ${featureName} to ${newFeatureName}`,
);
await this.validateName(newFeatureName);
const cToggle =
await this.featureStrategiesStore.getFeatureToggleWithVariantEnvs(
featureName,
);
const newToggle = {
...cToggle,
name: newFeatureName,
variants: undefined,
};
const created = await this.createFeatureToggle(
projectId,
newToggle,
userName,
);
const variantTasks = newToggle.environments.map((e) => {
return this.featureEnvironmentStore.addVariantsToFeatureEnvironment(
newToggle.name,
e.name,
e.variants,
);
});
const strategyTasks = newToggle.environments.flatMap((e) =>
e.strategies.map((s) => {
if (
replaceGroupId &&
s.parameters &&
s.parameters.hasOwnProperty('groupId')
) {
s.parameters.groupId = newFeatureName;
}
const context = {
projectId,
featureName: newFeatureName,
environment: e.name,
};
return this.createStrategy(s, context, userName);
}),
);
await Promise.all([...strategyTasks, ...variantTasks]);
return created;
}
async updateFeatureToggle(
projectId: string,
updatedFeature: FeatureToggleDTO,
userName: string,
featureName: string,
): Promise<FeatureToggle> {
await this.validateFeatureBelongsToProject({ featureName, projectId });
this.logger.info(`${userName} updates feature toggle ${featureName}`);
const featureData = await featureMetadataSchema.validateAsync(
updatedFeature,
);
const preData = await this.featureToggleStore.get(featureName);
const featureToggle = await this.featureToggleStore.update(projectId, {
...featureData,
name: featureName,
});
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureMetadataUpdateEvent({
createdBy: userName,
data: featureToggle,
preData,
featureName,
project: projectId,
tags,
}),
);
return featureToggle;
}
async getFeatureCountForProject(projectId: string): Promise<number> {
return this.featureToggleStore.count({
archived: false,
project: projectId,
});
}
async removeAllStrategiesForEnv(
toggleName: string,
environment: string = DEFAULT_ENV,
): Promise<void> {
await this.featureStrategiesStore.removeAllStrategiesForFeatureEnv(
toggleName,
environment,
);
}
async getStrategy(strategyId: string): Promise<Saved<IStrategyConfig>> {
const strategy = await this.featureStrategiesStore.getStrategyById(
strategyId,
);
const segments = await this.segmentService.getByStrategy(strategyId);
let result: Saved<IStrategyConfig> = {
id: strategy.id,
name: strategy.strategyName,
constraints: strategy.constraints || [],
parameters: strategy.parameters,
variants: strategy.variants || [],
segments: [],
title: strategy.title,
disabled: strategy.disabled,
sortOrder: strategy.sortOrder,
};
if (segments && segments.length > 0) {
result = {
...result,
segments: segments.map((segment) => segment.id),
};
}
return result;
}
async getEnvironmentInfo(
project: string,
environment: string,
featureName: string,
): Promise<IFeatureEnvironmentInfo> {
const envMetadata =
await this.featureEnvironmentStore.getEnvironmentMetaData(
environment,
featureName,
);
const strategies =
await this.featureStrategiesStore.getStrategiesForFeatureEnv(
project,
featureName,
environment,
);
const defaultStrategy = await this.projectStore.getDefaultStrategy(
project,
environment,
);
return {
name: featureName,
environment,
enabled: envMetadata.enabled,
strategies,
defaultStrategy,
};
}
// todo: store events for this change.
async deleteEnvironment(
projectId: string,
environment: string,
): Promise<void> {
await this.featureStrategiesStore.deleteConfigurationsForProjectAndEnvironment(
projectId,
environment,
);
await this.projectStore.deleteEnvironmentForProject(
projectId,
environment,
);
}
/** Validations */
async validateName(name: string): Promise<string> {
await nameSchema.validateAsync({ name });
await this.validateUniqueFeatureName(name);
return name;
}
async validateUniqueFeatureName(name: string): Promise<void> {
let msg: string;
try {
const feature = await this.featureToggleStore.get(name);
msg = feature.archived
? 'An archived toggle with that name already exists'
: 'A toggle with that name already exists';
} catch (error) {
return;
}
throw new NameExistsError(msg);
}
async hasFeature(name: string): Promise<boolean> {
return this.featureToggleStore.exists(name);
}
async updateStale(
featureName: string,
isStale: boolean,
createdBy: string,
): Promise<any> {
const feature = await this.featureToggleStore.get(featureName);
const { project } = feature;
feature.stale = isStale;
await this.featureToggleStore.update(project, feature);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureStaleEvent({
stale: isStale,
project,
featureName,
createdBy,
tags,
}),
);
return feature;
}
async archiveToggle(
featureName: string,
createdBy: string,
projectId?: string,
): Promise<void> {
const feature = await this.featureToggleStore.get(featureName);
if (projectId) {
await this.validateFeatureBelongsToProject({
featureName,
projectId,
});
}
await this.featureToggleStore.archive(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureArchivedEvent({
featureName,
createdBy,
project: feature.project,
tags,
}),
);
}
async archiveToggles(
featureNames: string[],
createdBy: string,
projectId: string,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
const features = await this.featureToggleStore.getAllByNames(
featureNames,
);
await this.featureToggleStore.batchArchive(featureNames);
const tags = await this.tagStore.getAllByFeatures(featureNames);
await this.eventStore.batchStore(
features.map(
(feature) =>
new FeatureArchivedEvent({
featureName: feature.name,
createdBy,
project: feature.project,
tags: tags
.filter((tag) => tag.featureName === feature.name)
.map((tag) => ({
value: tag.tagValue,
type: tag.tagType,
})),
}),
),
);
}
async setToggleStaleness(
featureNames: string[],
stale: boolean,
createdBy: string,
projectId: string,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
const features = await this.featureToggleStore.getAllByNames(
featureNames,
);
const relevantFeatures = features.filter(
(feature) => feature.stale !== stale,
);
const relevantFeatureNames = relevantFeatures.map(
(feature) => feature.name,
);
await this.featureToggleStore.batchStale(relevantFeatureNames, stale);
const tags = await this.tagStore.getAllByFeatures(relevantFeatureNames);
await this.eventStore.batchStore(
relevantFeatures.map(
(feature) =>
new FeatureStaleEvent({
stale: stale,
project: projectId,
featureName: feature.name,
createdBy,
tags: tags
.filter((tag) => tag.featureName === feature.name)
.map((tag) => ({
value: tag.tagValue,
type: tag.tagType,
})),
}),
),
);
}
async bulkUpdateEnabled(
project: string,
featureNames: string[],
environment: string,
enabled: boolean,
createdBy: string,
user?: User,
shouldActivateDisabledStrategies = false,
): Promise<void> {
await Promise.all(
featureNames.map((featureName) =>
this.updateEnabled(
project,
featureName,
environment,
enabled,
createdBy,
user,
shouldActivateDisabledStrategies,
),
),
);
}
async updateEnabled(
project: string,
featureName: string,
environment: string,
enabled: boolean,
createdBy: string,
user?: User,
shouldActivateDisabledStrategies = false,
): Promise<FeatureToggle> {
await this.stopWhenChangeRequestsEnabled(project, environment, user);
if (enabled) {
await this.stopWhenCannotCreateStrategies(
project,
environment,
featureName,
user,
);
}
return this.unprotectedUpdateEnabled(
project,
featureName,
environment,
enabled,
createdBy,
shouldActivateDisabledStrategies,
);
}
async unprotectedUpdateEnabled(
project: string,
featureName: string,
environment: string,
enabled: boolean,
createdBy: string,
shouldActivateDisabledStrategies = false,
): Promise<FeatureToggle> {
const hasEnvironment =
await this.featureEnvironmentStore.featureHasEnvironment(
environment,
featureName,
);
if (!hasEnvironment) {
throw new NotFoundError(
`Could not find environment ${environment} for feature: ${featureName}`,
);
}
if (enabled) {
const strategies = await this.getStrategiesForEnvironment(
project,
featureName,
environment,
);
const hasDisabledStrategies = strategies.some(
(strategy) => strategy.disabled,
);
if (hasDisabledStrategies && shouldActivateDisabledStrategies) {
strategies.map(async (strategy) => {
return this.updateStrategy(
strategy.id,
{ disabled: false },
{
environment,
projectId: project,
featureName,
},
createdBy,
);
});
}
const hasOnlyDisabledStrategies = strategies.every(
(strategy) => strategy.disabled,
);
const shouldCreate =
hasOnlyDisabledStrategies && !shouldActivateDisabledStrategies;
if (strategies.length === 0 || shouldCreate) {
const projectEnvironmentDefaultStrategy =
await this.projectStore.getDefaultStrategy(
project,
environment,
);
const strategy =
projectEnvironmentDefaultStrategy != null
? getProjectDefaultStrategy(
projectEnvironmentDefaultStrategy,
featureName,
)
: getDefaultStrategy(featureName);
await this.unprotectedCreateStrategy(
strategy,
{
environment,
projectId: project,
featureName,
},
createdBy,
);
}
}
const updatedEnvironmentStatus =
await this.featureEnvironmentStore.setEnvironmentEnabledStatus(
environment,
featureName,
enabled,
);
const feature = await this.featureToggleStore.get(featureName);
if (updatedEnvironmentStatus > 0) {
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureEnvironmentEvent({
enabled,
project,
featureName,
environment,
createdBy,
tags,
}),
);
}
return feature;
}
// @deprecated
async storeFeatureUpdatedEventLegacy(
featureName: string,
createdBy: string,
): Promise<FeatureToggleLegacy> {
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const feature = await this.getFeatureToggleLegacy(featureName);
// Legacy event. Will not be used from v4.3.
// We do not include 'preData' on purpose.
await this.eventStore.store({
type: FEATURE_UPDATED,
createdBy,
featureName,
data: feature,
tags,
project: feature.project,
});
return feature;
}
// @deprecated
async toggle(
projectId: string,
featureName: string,
environment: string,
userName: string,
): Promise<FeatureToggle> {
await this.featureToggleStore.get(featureName);
const isEnabled =
await this.featureEnvironmentStore.isEnvironmentEnabled(
featureName,
environment,
);
return this.updateEnabled(
projectId,
featureName,
environment,
!isEnabled,
userName,
);
}
// @deprecated
async getFeatureToggleLegacy(
featureName: string,
): Promise<FeatureToggleLegacy> {
const feature =
await this.featureStrategiesStore.getFeatureToggleWithEnvs(
featureName,
);
const { environments, ...legacyFeature } = feature;
const defaultEnv = environments.find((e) => e.name === DEFAULT_ENV);
const strategies = defaultEnv?.strategies || [];
const enabled = defaultEnv?.enabled || false;
return { ...legacyFeature, enabled, strategies };
}
async changeProject(
featureName: string,
newProject: string,
createdBy: string,
): Promise<void> {
const changeRequestEnabled =
await this.changeRequestAccessReadModel.isChangeRequestsEnabledForProject(
newProject,
);
if (changeRequestEnabled) {
throw new ForbiddenError(
`Changing project not allowed. Project ${newProject} has change requests enabled.`,
);
}
const feature = await this.featureToggleStore.get(featureName);
const oldProject = feature.project;
feature.project = newProject;
await this.featureToggleStore.update(newProject, feature);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureChangeProjectEvent({
createdBy,
oldProject,
newProject,
featureName,
tags,
}),
);
}
async getArchivedFeatures(): Promise<FeatureToggle[]> {
return this.getFeatureToggles({}, undefined, true);
}
// TODO: add project id.
async deleteFeature(featureName: string, createdBy: string): Promise<void> {
const toggle = await this.featureToggleStore.get(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.featureToggleStore.delete(featureName);
await this.eventStore.store(
new FeatureDeletedEvent({
featureName,
project: toggle.project,
createdBy,
preData: toggle,
tags,
}),
);
}
async deleteFeatures(
featureNames: string[],
projectId: string,
createdBy: string,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
const features = await this.featureToggleStore.getAllByNames(
featureNames,
);
const eligibleFeatures = features.filter(
(toggle) => toggle.archivedAt !== null,
);
const eligibleFeatureNames = eligibleFeatures.map(
(toggle) => toggle.name,
);
const tags = await this.tagStore.getAllByFeatures(eligibleFeatureNames);
await this.featureToggleStore.batchDelete(eligibleFeatureNames);
await this.eventStore.batchStore(
eligibleFeatures.map(
(feature) =>
new FeatureDeletedEvent({
featureName: feature.name,
createdBy,
project: feature.project,
preData: feature,
tags: tags
.filter((tag) => tag.featureName === feature.name)
.map((tag) => ({
value: tag.tagValue,
type: tag.tagType,
})),
}),
),
);
}
async reviveFeatures(
featureNames: string[],
projectId: string,
createdBy: string,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
const features = await this.featureToggleStore.getAllByNames(
featureNames,
);
const eligibleFeatures = features.filter(
(toggle) => toggle.archivedAt !== null,
);
const eligibleFeatureNames = eligibleFeatures.map(
(toggle) => toggle.name,
);
const tags = await this.tagStore.getAllByFeatures(eligibleFeatureNames);
await this.featureToggleStore.batchRevive(eligibleFeatureNames);
await this.eventStore.batchStore(
eligibleFeatures.map(
(feature) =>
new FeatureRevivedEvent({
featureName: feature.name,
createdBy,
project: feature.project,
tags: tags
.filter((tag) => tag.featureName === feature.name)
.map((tag) => ({
value: tag.tagValue,
type: tag.tagType,
})),
}),
),
);
}
// TODO: add project id.
async reviveFeature(featureName: string, createdBy: string): Promise<void> {
const toggle = await this.featureToggleStore.revive(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureRevivedEvent({
createdBy,
featureName,
project: toggle.project,
tags,
}),
);
}
async getMetadataForAllFeatures(
archived: boolean,
userId: number,
): Promise<FeatureToggle[]> {
const features = await this.featureToggleStore.getAll({ archived });
if (this.flagResolver.isEnabled('privateProjects')) {
const projectAccess =
await this.privateProjectChecker.getUserAccessibleProjects(
userId,
);
if (projectAccess.mode === 'all') {
return features;
} else {
return features.filter((f) =>
projectAccess.projects.includes(f.project),
);
}
}
return features;
}
async getMetadataForAllFeaturesByProjectId(
archived: boolean,
project: string,
): Promise<FeatureToggle[]> {
return this.featureToggleStore.getAll({ archived, project });
}
async getProjectId(name: string): Promise<string | undefined> {
return this.featureToggleStore.getProjectId(name);
}
async updateFeatureStrategyProject(
featureName: string,
newProjectId: string,
): Promise<void> {
await this.featureStrategiesStore.setProjectForStrategiesBelongingToFeature(
featureName,
newProjectId,
);
}
async updateVariants(
featureName: string,
project: string,
newVariants: Operation[],
user: User,
): Promise<FeatureToggle> {
const ft =
await this.featureStrategiesStore.getFeatureToggleWithVariantEnvs(
featureName,
);
const promises = ft.environments.map((env) =>
this.updateVariantsOnEnv(
featureName,
project,
env.name,
newVariants,
user,
).then((resultingVariants) => (env.variants = resultingVariants)),
);
await Promise.all(promises);
ft.variants = ft.environments[0].variants;
return ft;
}
async updateVariantsOnEnv(
featureName: string,
project: string,
environment: string,
newVariants: Operation[],
user: User,
): Promise<IVariant[]> {
const oldVariants = await this.getVariantsForEnv(
featureName,
environment,
);
const { newDocument } = await applyPatch(
deepClone(oldVariants),
newVariants,
);
return this.crProtectedSaveVariantsOnEnv(
project,
featureName,
environment,
newDocument,
user,
oldVariants,
);
}
async saveVariants(
featureName: string,
project: string,
newVariants: IVariant[],
createdBy: string,
): Promise<FeatureToggle> {
await variantsArraySchema.validateAsync(newVariants);
const fixedVariants = this.fixVariantWeights(newVariants);
const oldVariants = await this.featureToggleStore.getVariants(
featureName,
);
const featureToggle = await this.featureToggleStore.saveVariants(
project,
featureName,
fixedVariants,
);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureVariantEvent({
project,
featureName,
createdBy,
tags,
oldVariants,
newVariants: featureToggle.variants as IVariant[],
}),
);
return featureToggle;
}
async saveVariantsOnEnv(
projectId: string,
featureName: string,
environment: string,
newVariants: IVariant[],
user: User,
oldVariants?: IVariant[],
): Promise<IVariant[]> {
await variantsArraySchema.validateAsync(newVariants);
const fixedVariants = this.fixVariantWeights(newVariants);
const theOldVariants: IVariant[] =
oldVariants ||
(
await this.featureEnvironmentStore.get({
featureName,
environment,
})
).variants ||
[];
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new EnvironmentVariantEvent({
featureName,
environment,
project: projectId,
createdBy: user,
oldVariants: theOldVariants,
newVariants: fixedVariants,
tags,
}),
);
await this.featureEnvironmentStore.setVariantsToFeatureEnvironments(
featureName,
[environment],
fixedVariants,
);
return fixedVariants;
}
async crProtectedSaveVariantsOnEnv(
projectId: string,
featureName: string,
environment: string,
newVariants: IVariant[],
user: User,
oldVariants?: IVariant[],
): Promise<IVariant[]> {
await this.stopWhenChangeRequestsEnabled(projectId, environment, user);
return this.saveVariantsOnEnv(
projectId,
featureName,
environment,
newVariants,
user,
oldVariants,
);
}
async crProtectedSetVariantsOnEnvs(
projectId: string,
featureName: string,
environments: string[],
newVariants: IVariant[],
user: User,
): Promise<IVariant[]> {
for (const env of environments) {
await this.stopWhenChangeRequestsEnabled(projectId, env);
}
return this.setVariantsOnEnvs(
projectId,
featureName,
environments,
newVariants,
user,
);
}
async setVariantsOnEnvs(
projectId: string,
featureName: string,
environments: string[],
newVariants: IVariant[],
user: User,
): Promise<IVariant[]> {
await variantsArraySchema.validateAsync(newVariants);
const fixedVariants = this.fixVariantWeights(newVariants);
const oldVariants: { [env: string]: IVariant[] } = {};
for (const env of environments) {
const featureEnv = await this.featureEnvironmentStore.get({
featureName,
environment: env,
});
oldVariants[env] = featureEnv.variants || [];
}
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.batchStore(
environments.map(
(environment) =>
new EnvironmentVariantEvent({
featureName,
environment,
project: projectId,
createdBy: user,
oldVariants: oldVariants[environment],
newVariants: fixedVariants,
tags,
}),
),
);
await this.featureEnvironmentStore.setVariantsToFeatureEnvironments(
featureName,
environments,
fixedVariants,
);
return fixedVariants;
}
fixVariantWeights(variants: IVariant[]): IVariant[] {
let variableVariants = variants.filter((x) => {
return x.weightType === WeightType.VARIABLE;
});
if (variants.length > 0 && variableVariants.length === 0) {
throw new BadDataError(
'There must be at least one "variable" variant',
);
}
let fixedVariants = variants.filter((x) => {
return x.weightType === WeightType.FIX;
});
let fixedWeights = fixedVariants.reduce((a, v) => a + v.weight, 0);
if (fixedWeights > 1000) {
throw new BadDataError(
'The traffic distribution total must equal 100%',
);
}
let averageWeight = Math.floor(
(1000 - fixedWeights) / variableVariants.length,
);
let remainder = (1000 - fixedWeights) % variableVariants.length;
variableVariants = variableVariants.map((x) => {
x.weight = averageWeight;
if (remainder > 0) {
x.weight += 1;
remainder--;
}
return x;
});
return variableVariants
.concat(fixedVariants)
.sort((a, b) => a.name.localeCompare(b.name));
}
private async stopWhenChangeRequestsEnabled(
project: string,
environment: string,
user?: User,
) {
const canBypass =
await this.changeRequestAccessReadModel.canBypassChangeRequest(
project,
environment,
user,
);
if (!canBypass) {
throw new PermissionError(SKIP_CHANGE_REQUEST);
}
}
private async stopWhenCannotCreateStrategies(
project: string,
environment: string,
featureName: string,
user?: User,
) {
const hasEnvironment =
await this.featureEnvironmentStore.featureHasEnvironment(
environment,
featureName,
);
if (hasEnvironment) {
const strategies = await this.getStrategiesForEnvironment(
project,
featureName,
environment,
);
if (strategies.length === 0) {
const canAddStrategies =
user &&
(await this.accessService.hasPermission(
user,
CREATE_FEATURE_STRATEGY,
project,
environment,
));
if (!canAddStrategies) {
throw new PermissionError(CREATE_FEATURE_STRATEGY);
}
}
}
}
async updatePotentiallyStaleFeatures(): Promise<void> {
const potentiallyStaleFeatures =
await this.featureToggleStore.updatePotentiallyStaleFeatures();
if (potentiallyStaleFeatures.length > 0) {
return this.eventStore.batchStore(
await Promise.all(
potentiallyStaleFeatures
.filter((feature) => feature.potentiallyStale)
.map(
async ({ name, project }) =>
new PotentiallyStaleOnEvent({
featureName: name,
project,
tags: await this.tagStore.getAllTagsForFeature(
name,
),
}),
),
),
);
}
}
}
export default FeatureToggleService;