UNPKG

unleash-server

Version:

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

196 lines • 9.08 kB
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