unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
196 lines • 9.08 kB
JavaScript
import { SegmentCreatedEvent, SegmentDeletedEvent, SegmentUpdatedEvent, SKIP_CHANGE_REQUEST, } from '../../types/index.js';
import NameExistsError from '../../error/name-exists-error.js';
import { segmentSchema } from '../../services/segment-schema.js';
import BadDataError from '../../error/bad-data-error.js';
import { NotFoundError, PermissionError } from '../../error/index.js';
import { throwExceedsLimitError } from '../../error/exceeds-limit-error.js';
export class SegmentService {
constructor({ segmentStore, featureStrategiesStore, }, changeRequestAccessReadModel, changeRequestSegmentUsageReadModel, config, eventService, privateProjectChecker, resourceLimitsService) {
this.segmentStore = segmentStore;
this.featureStrategiesStore = featureStrategiesStore;
this.eventService = eventService;
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.changeRequestSegmentUsageReadModel =
changeRequestSegmentUsageReadModel;
this.privateProjectChecker = privateProjectChecker;
this.flagResolver = config.flagResolver;
this.resourceLimitsService = resourceLimitsService;
this.config = config;
}
async get(id) {
const segment = await this.segmentStore.get(id);
if (segment === undefined) {
throw new NotFoundError(`Could find segment with id ${id}`);
}
return segment;
}
async getAll() {
return this.segmentStore.getAll(this.config.isEnterprise);
}
async getByStrategy(strategyId) {
return this.segmentStore.getByStrategy(strategyId);
}
async getVisibleStrategies(id, userId) {
const allStrategies = await this.getAllStrategies(id);
const accessibleProjects = await this.privateProjectChecker.getUserAccessibleProjects(userId);
if (accessibleProjects.mode === 'all') {
return allStrategies;
}
else {
const filter = (strategy) => accessibleProjects.projects.includes(strategy.projectId);
return {
strategies: allStrategies.strategies.filter(filter),
changeRequestStrategies: allStrategies.changeRequestStrategies.filter(filter),
};
}
}
async getAllStrategies(id) {
const strategies = await this.featureStrategiesStore.getStrategiesBySegment(id);
if (this.config.isEnterprise) {
const changeRequestStrategies = await this.changeRequestSegmentUsageReadModel.getStrategiesUsedInActiveChangeRequests(id);
return { strategies, changeRequestStrategies };
}
return { strategies, changeRequestStrategies: [] };
}
async isInUse(id) {
const { strategies, changeRequestStrategies } = await this.getAllStrategies(id);
return strategies.length > 0 || changeRequestStrategies.length > 0;
}
async validateSegmentLimit() {
const { segments: limit } = await this.resourceLimitsService.getResourceLimits();
const segmentCount = await this.segmentStore.count();
if (segmentCount >= limit) {
throwExceedsLimitError(this.config.eventBus, {
resource: 'segment',
limit,
});
}
}
async create(data, auditUser) {
await this.validateSegmentLimit();
const input = await segmentSchema.validateAsync(data);
this.validateSegmentValuesLimit(input);
await this.validateName(input.name);
const segment = await this.segmentStore.create(input, auditUser);
await this.eventService.storeEvent(new SegmentCreatedEvent({
data: segment,
project: segment.project,
auditUser,
}));
return segment;
}
async update(id, data, user, auditUser) {
const input = await segmentSchema.validateAsync(data);
await this.stopWhenChangeRequestsEnabled(input.project, user);
return this.unprotectedUpdate(id, data, auditUser);
}
async unprotectedUpdate(id, data, auditUser) {
const input = await segmentSchema.validateAsync(data);
this.validateSegmentValuesLimit(input);
const preData = await this.segmentStore.get(id);
if (preData === undefined) {
throw new NotFoundError(`Could not find segment with id ${id} to update`);
}
if (preData.name !== input.name) {
await this.validateName(input.name);
}
await this.validateSegmentProject(id, input);
const segment = await this.segmentStore.update(id, input);
await this.eventService.storeEvent(new SegmentUpdatedEvent({
data: segment,
preData,
project: segment.project,
auditUser,
}));
}
async delete(id, user, auditUser) {
const segment = await this.segmentStore.get(id);
if (segment === undefined) {
/// Already deleted
return;
}
await this.stopWhenChangeRequestsEnabled(segment.project, user);
await this.segmentStore.delete(id);
await this.eventService.storeEvent(new SegmentDeletedEvent({
preData: segment,
project: segment.project,
auditUser,
}));
}
async unprotectedDelete(id, auditUser) {
const segment = await this.segmentStore.get(id);
await this.segmentStore.delete(id);
await this.eventService.storeEvent(new SegmentDeletedEvent({
preData: segment,
auditUser,
}));
}
async cloneStrategySegments(sourceStrategyId, targetStrategyId) {
const sourceStrategySegments = await this.getByStrategy(sourceStrategyId);
await Promise.all(sourceStrategySegments.map((sourceStrategySegment) => {
return this.addToStrategy(sourceStrategySegment.id, targetStrategyId);
}));
}
// Used by unleash-enterprise.
async addToStrategy(id, strategyId) {
await this.validateStrategySegmentLimit(strategyId);
await this.segmentStore.addToStrategy(id, strategyId);
}
async updateStrategySegments(strategyId, segmentIds) {
if (segmentIds.length > this.config.strategySegmentsLimit) {
throw new BadDataError(`Strategies may not have more than ${this.config.strategySegmentsLimit} segments`);
}
const segments = await this.getByStrategy(strategyId);
const currentSegmentIds = segments.map((segment) => segment.id);
const segmentIdsToRemove = currentSegmentIds.filter((id) => !segmentIds.includes(id));
await Promise.all(segmentIdsToRemove.map((segmentId) => this.removeFromStrategy(segmentId, strategyId)));
const segmentIdsToAdd = segmentIds.filter((id) => !currentSegmentIds.includes(id));
await Promise.all(segmentIdsToAdd.map((segmentId) => this.addToStrategy(segmentId, strategyId)));
}
// Used by unleash-enterprise.
async removeFromStrategy(id, strategyId) {
await this.segmentStore.removeFromStrategy(id, strategyId);
}
async validateName(name) {
if (!name) {
throw new BadDataError('Segment name cannot be empty');
}
if (await this.segmentStore.existsByName(name)) {
throw new NameExistsError('Segment name already exists');
}
}
async validateStrategySegmentLimit(strategyId) {
const { strategySegmentsLimit } = this.config;
if ((await this.getByStrategy(strategyId)).length >=
strategySegmentsLimit) {
throw new BadDataError(`Strategies may not have more than ${strategySegmentsLimit} segments`);
}
}
validateSegmentValuesLimit(segment) {
const { segmentValuesLimit } = this.config;
const valuesCount = segment.constraints
.flatMap((constraint) => constraint.values?.length ?? 0)
.reduce((acc, length) => acc + length, 0);
if (valuesCount > segmentValuesLimit) {
throw new BadDataError(`Segments may not have more than ${segmentValuesLimit} values`);
}
}
async validateSegmentProject(id, segment) {
const { strategies, changeRequestStrategies } = await this.getAllStrategies(id);
const projectsUsed = new Set([strategies, changeRequestStrategies].flatMap((strats) => strats.map((strategy) => strategy.projectId)));
if (segment.project &&
(projectsUsed.size > 1 ||
(projectsUsed.size === 1 && !projectsUsed.has(segment.project)))) {
throw new BadDataError(`Invalid project. Segment is being used by strategies in other projects: ${Array.from(projectsUsed).join(', ')}`);
}
}
async stopWhenChangeRequestsEnabled(project, user) {
if (!project)
return;
const canBypass = await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(project, user);
if (!canBypass) {
throw new PermissionError(SKIP_CHANGE_REQUEST);
}
}
}
//# sourceMappingURL=segment-service.js.map