unleash-server
Version:
Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.
739 lines • 35 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const bad_data_error_1 = __importDefault(require("../error/bad-data-error"));
const name_exists_error_1 = __importDefault(require("../error/name-exists-error"));
const invalid_operation_error_1 = __importDefault(require("../error/invalid-operation-error"));
const db_error_1 = require("../error/db-error");
const feature_schema_1 = require("../schema/feature-schema");
const events_1 = require("../types/events");
const notfound_error_1 = __importDefault(require("../error/notfound-error"));
const model_1 = require("../types/model");
const constants_1 = require("../util/constants");
const fast_json_patch_1 = require("fast-json-patch");
const operation_denied_error_1 = require("../error/operation-denied-error");
const constraint_types_1 = require("../util/validators/constraint-types");
const helpers_1 = require("../util/feature-evaluator/helpers");
const permissions_1 = require("../types/permissions");
const no_access_error_1 = __importDefault(require("../error/no-access-error"));
const oneOf = (values, match) => {
return values.some((value) => value === match);
};
class FeatureToggleService {
constructor({ featureStrategiesStore, featureToggleStore, featureToggleClientStore, projectStore, eventStore, featureTagStore, featureEnvironmentStore, contextFieldStore, }, { getLogger }, segmentService, accessService) {
this.logger = getLogger('services/feature-toggle-service.ts');
this.featureStrategiesStore = featureStrategiesStore;
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;
}
async validateFeatureContext({ featureName, projectId, }) {
const id = await this.featureToggleStore.getProjectId(featureName);
if (id !== projectId) {
throw new invalid_operation_error_1.default(`The operation could not be completed. The feature exists, but the provided project id ("${projectId}") does not match the project that the feature belongs to ("${id}"). Try using "${id}" in the request URL instead of "${projectId}".`);
}
}
validateFeatureStrategyContext(strategy, { featureName, projectId }) {
if (strategy.projectId !== projectId) {
throw new invalid_operation_error_1.default('You can not change the projectId for an activation strategy.');
}
if (strategy.featureName !== featureName) {
throw new invalid_operation_error_1.default('You can not change the featureName for an activation strategy.');
}
}
async validateConstraints(constraints) {
const validations = constraints.map((constraint) => {
return this.validateConstraint(constraint);
});
return Promise.all(validations);
}
async validateConstraint(input) {
const constraint = await feature_schema_1.constraintSchema.validateAsync(input);
const { operator } = constraint;
const contextDefinition = await this.contextFieldStore.get(constraint.contextName);
if (oneOf(constants_1.NUM_OPERATORS, operator)) {
await (0, constraint_types_1.validateNumber)(constraint.value);
}
if (oneOf(constants_1.STRING_OPERATORS, operator)) {
await (0, constraint_types_1.validateString)(constraint.values);
}
if (oneOf(constants_1.SEMVER_OPERATORS, operator)) {
// Semver library is not asynchronous, so we do not
// need to await here.
(0, constraint_types_1.validateSemver)(constraint.value);
}
if (oneOf(constants_1.DATE_OPERATORS, operator)) {
await (0, constraint_types_1.validateDate)(constraint.value);
}
if (oneOf([...constants_1.DATE_OPERATORS, ...constants_1.SEMVER_OPERATORS, ...constants_1.NUM_OPERATORS], operator)) {
if (contextDefinition?.legalValues?.length > 0) {
(0, constraint_types_1.validateLegalValues)(contextDefinition.legalValues, constraint.value);
}
}
else {
if (contextDefinition?.legalValues?.length > 0) {
(0, constraint_types_1.validateLegalValues)(contextDefinition.legalValues, constraint.values);
}
}
return constraint;
}
async patchFeature(project, featureName, createdBy, operations) {
const featureToggle = await this.getFeatureMetadata(featureName);
if (operations.some((op) => op.path.indexOf('/variants') >= 0)) {
throw new operation_denied_error_1.OperationDeniedError(`Changing variants is done via PATCH operation to /api/admin/projects/:project/features/:feature/variants`);
}
const { newDocument } = (0, fast_json_patch_1.applyPatch)((0, fast_json_patch_1.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 events_1.FeatureStaleEvent({
stale: newDocument.stale,
project,
featureName,
createdBy,
tags,
}));
}
return updated;
}
featureStrategyToPublic(featureStrategy, segments = []) {
return {
id: featureStrategy.id,
name: featureStrategy.strategyName,
constraints: featureStrategy.constraints || [],
parameters: featureStrategy.parameters,
segments: segments.map((segment) => segment.id) ?? [],
};
}
async updateStrategiesSortOrder(featureName, sortOrders) {
await Promise.all(sortOrders.map(async ({ id, sortOrder }) => this.featureStrategiesStore.updateSortOrder(id, sortOrder)));
}
async createStrategy(strategyConfig, context, createdBy, user) {
await this.stopWhenChangeRequestsEnabled(context.projectId, context.environment, user);
return this.unprotectedCreateStrategy(strategyConfig, context, createdBy);
}
async unprotectedCreateStrategy(strategyConfig, context, createdBy) {
const { featureName, projectId, environment } = context;
await this.validateFeatureContext(context);
if (strategyConfig.constraints?.length > 0) {
strategyConfig.constraints = await this.validateConstraints(strategyConfig.constraints);
}
try {
const newFeatureStrategy = await this.featureStrategiesStore.createStrategyFeatureEnv({
strategyName: strategyConfig.name,
constraints: strategyConfig.constraints,
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 tags = await this.tagStore.getAllTagsForFeature(featureName);
const segments = await this.segmentService.getByStrategy(newFeatureStrategy.id);
const strategy = this.featureStrategyToPublic(newFeatureStrategy, segments);
await this.eventStore.store(new events_1.FeatureStrategyAddEvent({
project: projectId,
featureName,
createdBy,
environment,
data: strategy,
tags,
}));
return strategy;
}
catch (e) {
if (e.code === db_error_1.FOREIGN_KEY_VIOLATION) {
throw new bad_data_error_1.default('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
*/
async updateStrategy(id, updates, context, userName, user) {
await this.stopWhenChangeRequestsEnabled(context.projectId, context.environment, user);
return this.unprotectedUpdateStrategy(id, updates, context, userName);
}
async unprotectedUpdateStrategy(id, updates, context, userName) {
const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id);
this.validateFeatureStrategyContext(existingStrategy, context);
if (existingStrategy.id === id) {
if (updates.constraints?.length > 0) {
updates.constraints = await this.validateConstraints(updates.constraints);
}
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 events_1.FeatureStrategyUpdateEvent({
project: projectId,
featureName,
environment,
createdBy: userName,
data,
preData,
tags,
}));
return data;
}
throw new notfound_error_1.default(`Could not find strategy with id ${id}`);
}
async updateStrategyParameter(id, name, value, context, userName) {
const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id);
this.validateFeatureStrategyContext(existingStrategy, context);
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 events_1.FeatureStrategyUpdateEvent({
featureName,
project: projectId,
environment,
createdBy: userName,
data,
preData,
tags,
}));
return data;
}
throw new notfound_error_1.default(`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
*/
async deleteStrategy(id, context, createdBy, user) {
await this.stopWhenChangeRequestsEnabled(context.projectId, context.environment, user);
return this.unprotectedDeleteStrategy(id, context, createdBy);
}
async unprotectedDeleteStrategy(id, context, createdBy) {
const existingStrategy = await this.featureStrategiesStore.get(id);
const { featureName, projectId, environment } = context;
this.validateFeatureStrategyContext(existingStrategy, context);
await this.featureStrategiesStore.delete(id);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const preData = this.featureStrategyToPublic(existingStrategy);
await this.eventStore.store(new events_1.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, featureName, environment = constants_1.DEFAULT_ENV) {
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 = [];
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,
sortOrder: strat.sortOrder,
segments,
});
}
return result;
}
throw new notfound_error_1.default(`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.
*/
async getFeature({ featureName, archived, projectId, environmentVariants, userId, }) {
if (projectId) {
await this.validateFeatureContext({ 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) {
return this.featureToggleStore.getVariants(featureName);
}
async getVariantsForEnv(featureName, environment) {
const featureEnvironment = await this.featureEnvironmentStore.get({
featureName,
environment,
});
return featureEnvironment.variants || [];
}
async getFeatureMetadata(featureName) {
return this.featureToggleStore.get(featureName);
}
async getClientFeatures(query, includeIds) {
return this.featureToggleClientStore.getClient(query, includeIds);
}
/**
* @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, userId, archived = false) {
return this.featureToggleClientStore.getAdmin({
featureQuery: query,
userId,
archived,
});
}
async getFeatureOverview(params) {
return this.featureStrategiesStore.getFeatureOverview(params);
}
async getFeatureToggle(featureName) {
return this.featureStrategiesStore.getFeatureToggleWithEnvs(featureName);
}
async createFeatureToggle(projectId, value, createdBy, isValidated = false) {
this.logger.info(`${createdBy} creates feature toggle ${value.name}`);
await this.validateName(value.name);
const exists = await this.projectStore.hasProject(projectId);
if (exists) {
let featureData;
if (isValidated) {
featureData = value;
}
else {
featureData = await feature_schema_1.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);
environments.forEach(async (featureEnv) => {
await this.featureEnvironmentStore.addVariantsToFeatureEnvironment(featureName, featureEnv.environment, value.variants);
});
}
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(new events_1.FeatureCreatedEvent({
featureName,
createdBy,
project: projectId,
data: createdToggle,
tags,
}));
return createdToggle;
}
throw new notfound_error_1.default(`Project with id ${projectId} does not exist`);
}
async cloneFeatureToggle(featureName, projectId, newFeatureName, replaceGroupId = true, // eslint-disable-line
userName) {
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.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, updatedFeature, userName, featureName) {
await this.validateFeatureContext({ featureName, projectId });
this.logger.info(`${userName} updates feature toggle ${featureName}`);
const featureData = await feature_schema_1.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 events_1.FeatureMetadataUpdateEvent({
createdBy: userName,
data: featureToggle,
preData,
featureName,
project: projectId,
tags,
}));
return featureToggle;
}
async getFeatureCountForProject(projectId) {
return this.featureToggleStore.count({
archived: false,
project: projectId,
});
}
async removeAllStrategiesForEnv(toggleName, environment = constants_1.DEFAULT_ENV) {
await this.featureStrategiesStore.removeAllStrategiesForFeatureEnv(toggleName, environment);
}
async getStrategy(strategyId) {
const strategy = await this.featureStrategiesStore.getStrategyById(strategyId);
const segments = await this.segmentService.getByStrategy(strategyId);
let result = {
id: strategy.id,
name: strategy.strategyName,
constraints: strategy.constraints || [],
parameters: strategy.parameters,
segments: [],
};
if (segments && segments.length > 0) {
result = {
...result,
segments: segments.map((segment) => segment.id),
};
}
return result;
}
async getEnvironmentInfo(project, environment, featureName) {
const envMetadata = await this.featureEnvironmentStore.getEnvironmentMetaData(environment, featureName);
const strategies = await this.featureStrategiesStore.getStrategiesForFeatureEnv(project, featureName, environment);
return {
name: featureName,
environment,
enabled: envMetadata.enabled,
strategies,
};
}
// todo: store events for this change.
async deleteEnvironment(projectId, environment) {
await this.featureStrategiesStore.deleteConfigurationsForProjectAndEnvironment(projectId, environment);
await this.projectStore.deleteEnvironmentForProject(projectId, environment);
}
/** Validations */
async validateName(name) {
await feature_schema_1.nameSchema.validateAsync({ name });
await this.validateUniqueFeatureName(name);
return name;
}
async validateUniqueFeatureName(name) {
let msg;
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 name_exists_error_1.default(msg);
}
async hasFeature(name) {
return this.featureToggleStore.exists(name);
}
async updateStale(featureName, isStale, createdBy) {
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 events_1.FeatureStaleEvent({
stale: isStale,
project,
featureName,
createdBy,
tags,
}));
return feature;
}
async archiveToggle(featureName, createdBy, projectId) {
const feature = await this.featureToggleStore.get(featureName);
if (projectId) {
await this.validateFeatureContext({ featureName, projectId });
}
await this.featureToggleStore.archive(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(new events_1.FeatureArchivedEvent({
featureName,
createdBy,
project: feature.project,
tags,
}));
}
async updateEnabled(project, featureName, environment, enabled, createdBy, user) {
await this.stopWhenChangeRequestsEnabled(project, environment, user);
if (enabled) {
await this.stopWhenCannotCreateStrategies(project, environment, featureName, user);
}
return this.unprotectedUpdateEnabled(project, featureName, environment, enabled, createdBy);
}
async unprotectedUpdateEnabled(project, featureName, environment, enabled, createdBy) {
const hasEnvironment = await this.featureEnvironmentStore.featureHasEnvironment(environment, featureName);
if (!hasEnvironment) {
throw new notfound_error_1.default(`Could not find environment ${environment} for feature: ${featureName}`);
}
if (enabled) {
const strategies = await this.getStrategiesForEnvironment(project, featureName, environment);
if (strategies.length === 0) {
await this.unprotectedCreateStrategy((0, helpers_1.getDefaultStrategy)(featureName), {
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 events_1.FeatureEnvironmentEvent({
enabled,
project,
featureName,
environment,
createdBy,
tags,
}));
}
return feature;
}
// @deprecated
async storeFeatureUpdatedEventLegacy(featureName, createdBy) {
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: events_1.FEATURE_UPDATED,
createdBy,
featureName,
data: feature,
tags,
project: feature.project,
});
return feature;
}
// @deprecated
async toggle(projectId, featureName, environment, userName) {
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) {
const feature = await this.featureStrategiesStore.getFeatureToggleWithEnvs(featureName);
const { environments, ...legacyFeature } = feature;
const defaultEnv = environments.find((e) => e.name === constants_1.DEFAULT_ENV);
const strategies = defaultEnv?.strategies || [];
const enabled = defaultEnv?.enabled || false;
return { ...legacyFeature, enabled, strategies };
}
async changeProject(featureName, newProject, createdBy) {
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 events_1.FeatureChangeProjectEvent({
createdBy,
oldProject,
newProject,
featureName,
tags,
}));
}
async getArchivedFeatures() {
return this.getFeatureToggles({}, undefined, true);
}
// TODO: add project id.
async deleteFeature(featureName, createdBy) {
const toggle = await this.featureToggleStore.get(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.featureToggleStore.delete(featureName);
await this.eventStore.store(new events_1.FeatureDeletedEvent({
featureName,
project: toggle.project,
createdBy,
preData: toggle,
tags,
}));
}
// TODO: add project id.
async reviveToggle(featureName, createdBy) {
const toggle = await this.featureToggleStore.revive(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(new events_1.FeatureRevivedEvent({
createdBy,
featureName,
project: toggle.project,
tags,
}));
}
async getMetadataForAllFeatures(archived) {
return this.featureToggleStore.getAll({ archived });
}
async getMetadataForAllFeaturesByProjectId(archived, project) {
return this.featureToggleStore.getAll({ archived, project });
}
async getProjectId(name) {
return this.featureToggleStore.getProjectId(name);
}
async updateFeatureStrategyProject(featureName, newProjectId) {
await this.featureStrategiesStore.setProjectForStrategiesBelongingToFeature(featureName, newProjectId);
}
async updateVariants(featureName, project, newVariants, createdBy) {
const ft = await this.featureStrategiesStore.getFeatureToggleWithVariantEnvs(featureName);
const promises = ft.environments.map((env) => this.updateVariantsOnEnv(featureName, project, env.name, newVariants, createdBy).then((resultingVariants) => (env.variants = resultingVariants)));
await Promise.all(promises);
ft.variants = ft.environments[0].variants;
return ft;
}
async updateVariantsOnEnv(featureName, project, environment, newVariants, createdBy) {
const oldVariants = await this.getVariantsForEnv(featureName, environment);
const { newDocument } = await (0, fast_json_patch_1.applyPatch)(oldVariants, newVariants);
return this.saveVariantsOnEnv(project, featureName, environment, newDocument, createdBy, oldVariants);
}
async saveVariants(featureName, project, newVariants, createdBy) {
await feature_schema_1.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 events_1.FeatureVariantEvent({
project,
featureName,
createdBy,
tags,
oldVariants,
newVariants: featureToggle.variants,
}));
return featureToggle;
}
async saveVariantsOnEnv(projectId, featureName, environment, newVariants, createdBy, oldVariants) {
await feature_schema_1.variantsArraySchema.validateAsync(newVariants);
const fixedVariants = this.fixVariantWeights(newVariants);
const theOldVariants = oldVariants ||
(await this.featureEnvironmentStore.get({
featureName,
environment,
})).variants;
await this.eventStore.store(new events_1.EnvironmentVariantEvent({
featureName,
environment,
project: projectId,
createdBy,
oldVariants: theOldVariants,
newVariants: fixedVariants,
}));
await this.featureEnvironmentStore.addVariantsToFeatureEnvironment(featureName, environment, fixedVariants);
return fixedVariants;
}
fixVariantWeights(variants) {
let variableVariants = variants.filter((x) => {
return x.weightType === model_1.WeightType.VARIABLE;
});
if (variants.length > 0 && variableVariants.length === 0) {
throw new bad_data_error_1.default('There must be at least one "variable" variant');
}
let fixedVariants = variants.filter((x) => {
return x.weightType === model_1.WeightType.FIX;
});
let fixedWeights = fixedVariants.reduce((a, v) => a + v.weight, 0);
if (fixedWeights > 1000) {
throw new bad_data_error_1.default('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));
}
async stopWhenChangeRequestsEnabled(project, environment, user) {
const [canSkipChangeRequest, changeRequestEnabled] = await Promise.all([
user
? this.accessService.hasPermission(user, permissions_1.SKIP_CHANGE_REQUEST, project, environment)
: Promise.resolve(false),
this.accessService.isChangeRequestsEnabled(project, environment),
]);
if (changeRequestEnabled && !canSkipChangeRequest) {
throw new no_access_error_1.default(permissions_1.SKIP_CHANGE_REQUEST);
}
}
async stopWhenCannotCreateStrategies(project, environment, featureName, 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, permissions_1.CREATE_FEATURE_STRATEGY, project, environment));
if (!canAddStrategies) {
throw new no_access_error_1.default(permissions_1.CREATE_FEATURE_STRATEGY);
}
}
}
}
}
exports.default = FeatureToggleService;
//# sourceMappingURL=feature-toggle-service.js.map