1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00
unleash.unleash/src/lib/services/segment-service.ts

352 lines
12 KiB
TypeScript
Raw Normal View History

import { IUnleashConfig } from '../types/option';
import {
IClientSegment,
IFlagResolver,
IUnleashStores,
SKIP_CHANGE_REQUEST,
SYSTEM_USER,
} from '../types';
import { Logger } from '../logger';
import NameExistsError from '../error/name-exists-error';
import { ISegmentStore } from '../types/stores/segment-store';
import { ISegment } from '../types/model';
import { segmentSchema } from './segment-schema';
import {
SEGMENT_CREATED,
SEGMENT_DELETED,
SEGMENT_UPDATED,
} from '../types/events';
import User from '../types/user';
import { IFeatureStrategiesStore } from '../features/feature-toggle/types/feature-toggle-strategies-store-type';
import BadDataError from '../error/bad-data-error';
import {
ISegmentService,
StrategiesUsingSegment,
} from '../segments/segment-service-interface';
import { PermissionError } from '../error';
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model';
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
import EventService from '../features/events/event-service';
import { IChangeRequestSegmentUsageReadModel } from '../features/change-request-segment-usage-service/change-request-segment-usage-read-model';
export class SegmentService implements ISegmentService {
private logger: Logger;
private segmentStore: ISegmentStore;
private featureStrategiesStore: IFeatureStrategiesStore;
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
private changeRequestSegmentUsageReadModel: IChangeRequestSegmentUsageReadModel;
private config: IUnleashConfig;
private flagResolver: IFlagResolver;
private eventService: EventService;
private privateProjectChecker: IPrivateProjectChecker;
constructor(
{
segmentStore,
featureStrategiesStore,
}: Pick<IUnleashStores, 'segmentStore' | 'featureStrategiesStore'>,
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
changeRequestSegmentUsageReadModel: IChangeRequestSegmentUsageReadModel,
config: IUnleashConfig,
eventService: EventService,
privateProjectChecker: IPrivateProjectChecker,
) {
this.segmentStore = segmentStore;
this.featureStrategiesStore = featureStrategiesStore;
this.eventService = eventService;
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.changeRequestSegmentUsageReadModel =
changeRequestSegmentUsageReadModel;
this.privateProjectChecker = privateProjectChecker;
this.logger = config.getLogger('services/segment-service.ts');
this.flagResolver = config.flagResolver;
this.config = config;
}
async get(id: number): Promise<ISegment> {
return this.segmentStore.get(id);
}
async getAll(): Promise<ISegment[]> {
return this.segmentStore.getAll(this.config.isEnterprise);
}
async getActive(): Promise<ISegment[]> {
return this.segmentStore.getActive();
}
async getActiveForClient(): Promise<IClientSegment[]> {
return this.segmentStore.getActiveForClient();
}
async getByStrategy(strategyId: string): Promise<ISegment[]> {
return this.segmentStore.getByStrategy(strategyId);
}
async getVisibleStrategies(
id: number,
userId: number,
): Promise<StrategiesUsingSegment> {
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: number): Promise<StrategiesUsingSegment> {
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: number): Promise<boolean> {
const { strategies, changeRequestStrategies } =
await this.getAllStrategies(id);
return strategies.length > 0 || changeRequestStrategies.length > 0;
}
async create(
data: unknown,
user: Partial<Pick<User, 'id' | 'username' | 'email'>>,
): Promise<ISegment> {
const input = await segmentSchema.validateAsync(data);
this.validateSegmentValuesLimit(input);
await this.validateName(input.name);
const segment = await this.segmentStore.create(input, user);
await this.eventService.storeEvent({
type: SEGMENT_CREATED,
createdBy: user.email || user.username || SYSTEM_USER.username,
createdByUserId: user.id || SYSTEM_USER.id,
data: segment,
project: segment.project,
});
return segment;
}
async update(id: number, data: unknown, user: User): Promise<void> {
const input = await segmentSchema.validateAsync(data);
await this.stopWhenChangeRequestsEnabled(input.project, user);
return this.unprotectedUpdate(id, data, user);
}
async unprotectedUpdate(
id: number,
data: unknown,
user: User,
): Promise<void> {
const input = await segmentSchema.validateAsync(data);
this.validateSegmentValuesLimit(input);
const preData = await this.segmentStore.get(id);
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({
type: SEGMENT_UPDATED,
createdBy: user.email || user.username || 'unknown',
createdByUserId: user.id,
data: segment,
preData,
project: segment.project,
});
}
async delete(id: number, user: User): Promise<void> {
const segment = await this.segmentStore.get(id);
await this.stopWhenChangeRequestsEnabled(segment.project, user);
await this.segmentStore.delete(id);
await this.eventService.storeEvent({
type: SEGMENT_DELETED,
createdBy: user.email || user.username,
createdByUserId: user.id,
feat: add more events in integrations (#4815) https://linear.app/unleash/issue/2-1253/add-support-for-more-events-in-the-slack-app-integration Adds support for a lot more events in our integrations. Here is how the full list looks like: - ADDON_CONFIG_CREATED - ADDON_CONFIG_DELETED - ADDON_CONFIG_UPDATED - API_TOKEN_CREATED - API_TOKEN_DELETED - CHANGE_ADDED - CHANGE_DISCARDED - CHANGE_EDITED - CHANGE_REQUEST_APPLIED - CHANGE_REQUEST_APPROVAL_ADDED - CHANGE_REQUEST_APPROVED - CHANGE_REQUEST_CANCELLED - CHANGE_REQUEST_CREATED - CHANGE_REQUEST_DISCARDED - CHANGE_REQUEST_REJECTED - CHANGE_REQUEST_SENT_TO_REVIEW - CONTEXT_FIELD_CREATED - CONTEXT_FIELD_DELETED - CONTEXT_FIELD_UPDATED - FEATURE_ARCHIVED - FEATURE_CREATED - FEATURE_DELETED - FEATURE_ENVIRONMENT_DISABLED - FEATURE_ENVIRONMENT_ENABLED - FEATURE_ENVIRONMENT_VARIANTS_UPDATED - FEATURE_METADATA_UPDATED - FEATURE_POTENTIALLY_STALE_ON - FEATURE_PROJECT_CHANGE - FEATURE_REVIVED - FEATURE_STALE_OFF - FEATURE_STALE_ON - FEATURE_STRATEGY_ADD - FEATURE_STRATEGY_REMOVE - FEATURE_STRATEGY_UPDATE - FEATURE_TAGGED - FEATURE_UNTAGGED - GROUP_CREATED - GROUP_DELETED - GROUP_UPDATED - PROJECT_CREATED - PROJECT_DELETED - SEGMENT_CREATED - SEGMENT_DELETED - SEGMENT_UPDATED - SERVICE_ACCOUNT_CREATED - SERVICE_ACCOUNT_DELETED - SERVICE_ACCOUNT_UPDATED - USER_CREATED - USER_DELETED - USER_UPDATED I added the events that I thought were relevant based on my own discretion. Know of any event we should add? Let me know and I'll add it 🙂 For now I only added these events to the new Slack App integration, but we can add them to the other integrations as well since they are now supported. The event formatter was refactored and changed quite a bit in order to make it easier to maintain and add new events in the future. As a result, events are now posted with different text. Do we consider this a breaking change? If so, I can keep the old event formatter around, create a new one and only use it for the new Slack App integration. I noticed we don't have good 404 behaviors in the UI for things that are deleted in the meantime, that's why I avoided some links to specific resources (like feature strategies, integration configurations, etc), but we could add them later if we improve this. This PR also tries to add some consistency to the the way we log events.
2023-09-29 17:11:59 +02:00
preData: segment,
project: segment.project,
});
}
async unprotectedDelete(id: number, user: User): Promise<void> {
const segment = await this.segmentStore.get(id);
await this.segmentStore.delete(id);
await this.eventService.storeEvent({
type: SEGMENT_DELETED,
createdBy: user.email || user.username,
createdByUserId: user.id,
feat: add more events in integrations (#4815) https://linear.app/unleash/issue/2-1253/add-support-for-more-events-in-the-slack-app-integration Adds support for a lot more events in our integrations. Here is how the full list looks like: - ADDON_CONFIG_CREATED - ADDON_CONFIG_DELETED - ADDON_CONFIG_UPDATED - API_TOKEN_CREATED - API_TOKEN_DELETED - CHANGE_ADDED - CHANGE_DISCARDED - CHANGE_EDITED - CHANGE_REQUEST_APPLIED - CHANGE_REQUEST_APPROVAL_ADDED - CHANGE_REQUEST_APPROVED - CHANGE_REQUEST_CANCELLED - CHANGE_REQUEST_CREATED - CHANGE_REQUEST_DISCARDED - CHANGE_REQUEST_REJECTED - CHANGE_REQUEST_SENT_TO_REVIEW - CONTEXT_FIELD_CREATED - CONTEXT_FIELD_DELETED - CONTEXT_FIELD_UPDATED - FEATURE_ARCHIVED - FEATURE_CREATED - FEATURE_DELETED - FEATURE_ENVIRONMENT_DISABLED - FEATURE_ENVIRONMENT_ENABLED - FEATURE_ENVIRONMENT_VARIANTS_UPDATED - FEATURE_METADATA_UPDATED - FEATURE_POTENTIALLY_STALE_ON - FEATURE_PROJECT_CHANGE - FEATURE_REVIVED - FEATURE_STALE_OFF - FEATURE_STALE_ON - FEATURE_STRATEGY_ADD - FEATURE_STRATEGY_REMOVE - FEATURE_STRATEGY_UPDATE - FEATURE_TAGGED - FEATURE_UNTAGGED - GROUP_CREATED - GROUP_DELETED - GROUP_UPDATED - PROJECT_CREATED - PROJECT_DELETED - SEGMENT_CREATED - SEGMENT_DELETED - SEGMENT_UPDATED - SERVICE_ACCOUNT_CREATED - SERVICE_ACCOUNT_DELETED - SERVICE_ACCOUNT_UPDATED - USER_CREATED - USER_DELETED - USER_UPDATED I added the events that I thought were relevant based on my own discretion. Know of any event we should add? Let me know and I'll add it 🙂 For now I only added these events to the new Slack App integration, but we can add them to the other integrations as well since they are now supported. The event formatter was refactored and changed quite a bit in order to make it easier to maintain and add new events in the future. As a result, events are now posted with different text. Do we consider this a breaking change? If so, I can keep the old event formatter around, create a new one and only use it for the new Slack App integration. I noticed we don't have good 404 behaviors in the UI for things that are deleted in the meantime, that's why I avoided some links to specific resources (like feature strategies, integration configurations, etc), but we could add them later if we improve this. This PR also tries to add some consistency to the the way we log events.
2023-09-29 17:11:59 +02:00
preData: segment,
});
}
async cloneStrategySegments(
sourceStrategyId: string,
targetStrategyId: string,
): Promise<void> {
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: number, strategyId: string): Promise<void> {
await this.validateStrategySegmentLimit(strategyId);
await this.segmentStore.addToStrategy(id, strategyId);
}
async updateStrategySegments(
strategyId: string,
segmentIds: number[],
): Promise<void> {
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: number, strategyId: string): Promise<void> {
await this.segmentStore.removeFromStrategy(id, strategyId);
}
async validateName(name: string): Promise<void> {
if (!name) {
throw new BadDataError('Segment name cannot be empty');
}
if (await this.segmentStore.existsByName(name)) {
throw new NameExistsError('Segment name already exists');
}
}
private async validateStrategySegmentLimit(
strategyId: string,
): Promise<void> {
const { strategySegmentsLimit } = this.config;
if (
(await this.getByStrategy(strategyId)).length >=
strategySegmentsLimit
) {
throw new BadDataError(
`Strategies may not have more than ${strategySegmentsLimit} segments`,
);
}
}
private validateSegmentValuesLimit(segment: Omit<ISegment, 'id'>): void {
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`,
);
}
}
private async validateSegmentProject(
id: number,
segment: Omit<ISegment, 'id'>,
): Promise<void> {
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(', ')}`,
);
}
}
private async stopWhenChangeRequestsEnabled(project?: string, user?: User) {
if (!project) return;
const canBypass =
await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(
project,
user,
);
if (!canBypass) {
throw new PermissionError(SKIP_CHANGE_REQUEST);
}
}
}