mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: open-source segments 🚀 (#4690)
We love all open-source Unleash users. in 2022 we built the [segment capability](https://docs.getunleash.io/reference/segments) (v4.13) as an enterprise feature, simplify life for our customers. Now it is time to contribute it to the world 🌏 --------- Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
bff1bd1026
commit
013efac46b
@ -3,5 +3,4 @@ export const C = 'C';
|
||||
export const E = 'E';
|
||||
export const EEA = 'EEA';
|
||||
export const RE = 'RE';
|
||||
export const SE = 'SE';
|
||||
export const UG = 'UG';
|
||||
|
@ -141,8 +141,7 @@ export const FeatureStrategyEdit = () => {
|
||||
savedStrategySegments && setSegments(savedStrategySegments);
|
||||
}, [JSON.stringify(savedStrategySegments)]);
|
||||
|
||||
const segmentsToSubmit = uiConfig?.flags.SE ? segments : [];
|
||||
const payload = createStrategyPayload(strategy, segmentsToSubmit);
|
||||
const payload = createStrategyPayload(strategy, segments);
|
||||
|
||||
const onStrategyEdit = async (payload: IFeatureStrategyPayload) => {
|
||||
await updateStrategyOnFeature(
|
||||
|
@ -230,15 +230,10 @@ export const FeatureStrategyForm = ({
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(uiConfig.flags.SE)}
|
||||
show={
|
||||
<FeatureStrategySegment
|
||||
segments={segments}
|
||||
setSegments={setSegments}
|
||||
projectId={projectId}
|
||||
/>
|
||||
}
|
||||
<FeatureStrategySegment
|
||||
segments={segments}
|
||||
setSegments={setSegments}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<FeatureStrategyConstraints
|
||||
projectId={feature.project}
|
||||
|
@ -243,11 +243,9 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
|
||||
}
|
||||
|
||||
const listItems = [
|
||||
Boolean(uiConfig.flags.SE) &&
|
||||
strategySegments &&
|
||||
strategySegments.length > 0 && (
|
||||
<FeatureOverviewSegment segments={strategySegments} />
|
||||
),
|
||||
strategySegments && strategySegments.length > 0 && (
|
||||
<FeatureOverviewSegment segments={strategySegments} />
|
||||
),
|
||||
constraints.length > 0 && (
|
||||
<ConstraintAccordionList
|
||||
constraints={constraints}
|
||||
|
@ -355,7 +355,6 @@ exports[`returns all baseRoutes 1`] = `
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"flag": "SE",
|
||||
"hidden": false,
|
||||
"layout": "main",
|
||||
"menu": {},
|
||||
@ -365,7 +364,6 @@ exports[`returns all baseRoutes 1`] = `
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"flag": "SE",
|
||||
"hidden": false,
|
||||
"layout": "main",
|
||||
"menu": {},
|
||||
@ -375,7 +373,6 @@ exports[`returns all baseRoutes 1`] = `
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"flag": "SE",
|
||||
"hidden": false,
|
||||
"menu": {
|
||||
"advanced": true,
|
||||
|
@ -4,7 +4,7 @@ import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesLi
|
||||
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
|
||||
import { IntegrationList } from 'component/integrations/IntegrationList/IntegrationList';
|
||||
import Login from 'component/user/Login/Login';
|
||||
import { EEA, P, SE } from 'component/common/flags';
|
||||
import { EEA, P } from 'component/common/flags';
|
||||
import { NewUser } from 'component/user/NewUser/NewUser';
|
||||
import ResetPassword from 'component/user/ResetPassword/ResetPassword';
|
||||
import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword';
|
||||
@ -374,7 +374,6 @@ export const routes: IRoute[] = [
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
flag: SE,
|
||||
},
|
||||
{
|
||||
path: '/segments/edit/:segmentId',
|
||||
@ -384,7 +383,6 @@ export const routes: IRoute[] = [
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
flag: SE,
|
||||
},
|
||||
{
|
||||
path: '/segments',
|
||||
@ -393,7 +391,6 @@ export const routes: IRoute[] = [
|
||||
hidden: false,
|
||||
type: 'protected',
|
||||
menu: { mobile: true, advanced: true },
|
||||
flag: SE,
|
||||
},
|
||||
|
||||
// History
|
||||
|
@ -3,7 +3,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||
import { styled } from '@mui/material';
|
||||
import { PlaygroundRequestSchema, PlaygroundStrategySchema } from 'openapi';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { ConstraintExecution } from './ConstraintExecution/ConstraintExecution';
|
||||
import { SegmentExecution } from './SegmentExecution/SegmentExecution';
|
||||
import { PlaygroundResultStrategyExecutionParameters } from './StrategyExecutionParameters/StrategyExecutionParameters';
|
||||
@ -28,10 +27,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
|
||||
}) => {
|
||||
const { name, constraints, segments, parameters } = strategyResult;
|
||||
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const hasSegments =
|
||||
Boolean(uiConfig.flags.SE) && Boolean(segments && segments.length > 0);
|
||||
const hasSegments = Boolean(segments && segments.length > 0);
|
||||
const hasConstraints = Boolean(constraints && constraints?.length > 0);
|
||||
const hasExecutionParameters =
|
||||
name !== 'default' &&
|
||||
|
@ -86,11 +86,7 @@ const EditDefaultStrategy = () => {
|
||||
}
|
||||
}, [JSON.stringify(allSegments), JSON.stringify(strategy?.segments)]);
|
||||
|
||||
const segmentsToSubmit = uiConfig?.flags.SE ? segments : [];
|
||||
const payload = createStrategyPayload(
|
||||
defaultStrategy as any,
|
||||
segmentsToSubmit
|
||||
);
|
||||
const payload = createStrategyPayload(defaultStrategy as any, segments);
|
||||
|
||||
const onDefaultStrategyEdit = async (
|
||||
payload: CreateFeatureStrategySchema
|
||||
|
@ -155,15 +155,10 @@ export const ProjectDefaultStrategyForm = ({
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(uiConfig.flags.SE)}
|
||||
show={
|
||||
<FeatureStrategySegment
|
||||
segments={segments}
|
||||
setSegments={setSegments}
|
||||
projectId={projectId}
|
||||
/>
|
||||
}
|
||||
<FeatureStrategySegment
|
||||
segments={segments}
|
||||
setSegments={setSegments}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<FeatureStrategyConstraints
|
||||
projectId={projectId}
|
||||
|
@ -37,6 +37,7 @@ export const SegmentProjectAlert = ({
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const projectList = (
|
||||
<StyledUl>
|
||||
{Array.from(projectsUsed).map(projectId => (
|
||||
@ -87,17 +88,6 @@ export const SegmentProjectAlert = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (availableProjects.length === 1) {
|
||||
return (
|
||||
<StyledAlert severity="info">
|
||||
You can't specify a project other than{' '}
|
||||
<strong>{availableProjects[0].name}</strong> for this segment
|
||||
because it is used here:
|
||||
{projectList}
|
||||
</StyledAlert>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useCallback } from 'react';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { ISegment } from 'interfaces/segment';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||
|
||||
export interface IUseSegmentsOutput {
|
||||
segments?: ISegment[];
|
||||
@ -19,15 +19,9 @@ export const useSegments = (strategyId?: string): IUseSegmentsOutput => {
|
||||
? formatApiPath(`api/admin/segments/strategies/${strategyId}`)
|
||||
: formatApiPath('api/admin/segments');
|
||||
|
||||
const { data, error, mutate } = useConditionalSWR(
|
||||
Boolean(uiConfig.flags?.SE),
|
||||
[],
|
||||
url,
|
||||
() => fetchSegments(url),
|
||||
{
|
||||
refreshInterval: 15 * 1000,
|
||||
}
|
||||
);
|
||||
const { data, error, mutate } = useSWR(url, () => fetchSegments(url), {
|
||||
refreshInterval: 15 * 1000,
|
||||
});
|
||||
|
||||
const refetchSegments = useCallback(() => {
|
||||
mutate().catch(console.warn);
|
||||
|
@ -40,7 +40,6 @@ export type UiFlags = {
|
||||
P: boolean;
|
||||
RE: boolean;
|
||||
EEA?: boolean;
|
||||
SE?: boolean;
|
||||
T?: boolean;
|
||||
UNLEASH_CLOUD?: boolean;
|
||||
UG?: boolean;
|
||||
|
468
src/lib/features/segment/segment-controller.ts
Normal file
468
src/lib/features/segment/segment-controller.ts
Normal file
@ -0,0 +1,468 @@
|
||||
import { Request, Response } from 'express';
|
||||
import Controller from '../../routes/controller';
|
||||
|
||||
import {
|
||||
IAuthRequest,
|
||||
IUnleashConfig,
|
||||
IUnleashServices,
|
||||
Logger,
|
||||
} from '../../server-impl';
|
||||
import {
|
||||
AdminSegmentSchema,
|
||||
UpdateFeatureStrategySegmentsSchema,
|
||||
UpsertSegmentSchema,
|
||||
adminSegmentSchema,
|
||||
createRequestSchema,
|
||||
createResponseSchema,
|
||||
resourceCreatedResponseSchema,
|
||||
updateFeatureStrategySchema,
|
||||
} from '../../openapi';
|
||||
import {
|
||||
emptyResponse,
|
||||
getStandardResponses,
|
||||
} from '../../openapi/util/standard-responses';
|
||||
import { ISegmentService } from '../../segments/segment-service-interface';
|
||||
import { SegmentStrategiesSchema } from '../../openapi/spec/admin-strategies-schema';
|
||||
import { AccessService, OpenApiService } from '../../services';
|
||||
import {
|
||||
CREATE_SEGMENT,
|
||||
DELETE_SEGMENT,
|
||||
IFlagResolver,
|
||||
NONE,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
UPDATE_SEGMENT,
|
||||
serializeDates,
|
||||
} from '../../types';
|
||||
import {
|
||||
segmentsSchema,
|
||||
SegmentsSchema,
|
||||
} from '../../openapi/spec/segments-schema';
|
||||
|
||||
import { anonymiseKeys } from '../../util';
|
||||
import { BadDataError } from '../../error';
|
||||
|
||||
type IUpdateFeatureStrategySegmentsRequest = IAuthRequest<
|
||||
{},
|
||||
undefined,
|
||||
UpdateFeatureStrategySegmentsSchema
|
||||
>;
|
||||
|
||||
export class SegmentsController extends Controller {
|
||||
private logger: Logger;
|
||||
|
||||
private segmentService: ISegmentService;
|
||||
|
||||
private accessService: AccessService;
|
||||
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
segmentService,
|
||||
accessService,
|
||||
openApiService,
|
||||
}: Pick<
|
||||
IUnleashServices,
|
||||
'segmentService' | 'accessService' | 'openApiService'
|
||||
>,
|
||||
) {
|
||||
super(config);
|
||||
this.flagResolver = config.flagResolver;
|
||||
this.config = config;
|
||||
this.logger = config.getLogger('/admin-api/segments.ts');
|
||||
this.segmentService = segmentService;
|
||||
this.accessService = accessService;
|
||||
this.openApiService = openApiService;
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/validate',
|
||||
handler: this.validate,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
summary: 'Validates if a segment name exists',
|
||||
description:
|
||||
'Uses the name provided in the body of the request to validate if the given name exists or not',
|
||||
tags: ['Segments'],
|
||||
operationId: 'validateSegment',
|
||||
requestBody: createRequestSchema('nameSchema'),
|
||||
responses: {
|
||||
204: emptyResponse,
|
||||
...getStandardResponses(400, 401, 409, 415),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/strategies/:strategyId',
|
||||
handler: this.getSegmentsByStrategy,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Segments'],
|
||||
operationId: 'getSegmentsByStrategyId',
|
||||
summary: 'Get strategy segments',
|
||||
description:
|
||||
"Retrieve all segments that are referenced by the specified strategy. Returns an empty list of segments if the strategy ID doesn't exist.",
|
||||
responses: {
|
||||
200: createResponseSchema('segmentsSchema'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/strategies',
|
||||
handler: this.updateFeatureStrategySegments,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
summary: 'Update strategy segments',
|
||||
description:
|
||||
'Sets the segments of the strategy specified to be exactly the ones passed in the payload. Any segments that were used by the strategy before will be removed if they are not in the provided list of segments.',
|
||||
tags: ['Strategies'],
|
||||
operationId: 'updateFeatureStrategySegments',
|
||||
requestBody: createRequestSchema(
|
||||
'updateFeatureStrategySegmentsSchema',
|
||||
),
|
||||
responses: {
|
||||
201: resourceCreatedResponseSchema(
|
||||
'updateFeatureStrategySegmentsSchema',
|
||||
),
|
||||
...getStandardResponses(400, 401, 403, 415),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:id/strategies',
|
||||
handler: this.getStrategiesBySegment,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Segments'],
|
||||
operationId: 'getStrategiesBySegmentId',
|
||||
summary: 'Get strategies that reference segment',
|
||||
description:
|
||||
'Retrieve all strategies that reference the specified segment.',
|
||||
responses: {
|
||||
200: createResponseSchema('adminStrategiesSchema'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'delete',
|
||||
path: '/:id',
|
||||
handler: this.removeSegment,
|
||||
permission: DELETE_SEGMENT,
|
||||
acceptAnyContentType: true,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
summary: 'Deletes a segment by id',
|
||||
description:
|
||||
'Deletes a segment by its id, if not found returns a 409 error',
|
||||
tags: ['Segments'],
|
||||
operationId: 'removeSegment',
|
||||
responses: {
|
||||
204: emptyResponse,
|
||||
...getStandardResponses(401, 403, 409),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'put',
|
||||
path: '/:id',
|
||||
handler: this.updateSegment,
|
||||
permission: UPDATE_SEGMENT,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
summary: 'Update segment by id',
|
||||
description:
|
||||
'Updates the content of the segment with the provided payload. Any fields not specified will be left untouched.',
|
||||
tags: ['Segments'],
|
||||
operationId: 'updateSegment',
|
||||
responses: {
|
||||
204: emptyResponse,
|
||||
...getStandardResponses(400, 401, 403, 409, 415),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:id',
|
||||
handler: this.getSegment,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
summary: 'Get a segment',
|
||||
description: 'Retrieves a segment based on its ID.',
|
||||
tags: ['Segments'],
|
||||
operationId: 'getSegment',
|
||||
responses: {
|
||||
200: createResponseSchema('adminSegmentSchema'),
|
||||
...getStandardResponses(404),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '',
|
||||
handler: this.createSegment,
|
||||
permission: CREATE_SEGMENT,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
summary: 'Create a new segment',
|
||||
description:
|
||||
'Creates a new segment using the payload provided',
|
||||
tags: ['Segments'],
|
||||
operationId: 'createSegment',
|
||||
requestBody: createRequestSchema('upsertSegmentSchema'),
|
||||
responses: {
|
||||
201: resourceCreatedResponseSchema(
|
||||
'adminSegmentSchema',
|
||||
),
|
||||
...getStandardResponses(400, 401, 403, 409, 415),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '',
|
||||
handler: this.getSegments,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
summary: 'Get all segments',
|
||||
description:
|
||||
'Retrieves all segments that exist in this Unleash instance.',
|
||||
tags: ['Segments'],
|
||||
operationId: 'getSegments',
|
||||
responses: {
|
||||
200: createResponseSchema('segmentsSchema'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
req: Request<unknown, unknown, { name: string }>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { name } = req.body;
|
||||
await this.segmentService.validateName(name);
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async getSegmentsByStrategy(
|
||||
req: Request<{ strategyId: string }>,
|
||||
res: Response<SegmentsSchema>,
|
||||
): Promise<void> {
|
||||
const { strategyId } = req.params;
|
||||
const segments = await this.segmentService.getByStrategy(strategyId);
|
||||
|
||||
const responseBody = this.flagResolver.isEnabled('anonymiseEventLog')
|
||||
? {
|
||||
segments: anonymiseKeys(segments, ['createdBy']),
|
||||
}
|
||||
: { segments };
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
segmentsSchema.$id,
|
||||
serializeDates(responseBody),
|
||||
);
|
||||
}
|
||||
|
||||
async updateFeatureStrategySegments(
|
||||
req: IUpdateFeatureStrategySegmentsRequest,
|
||||
res: Response<UpdateFeatureStrategySegmentsSchema>,
|
||||
): Promise<void> {
|
||||
const { projectId, environmentId, strategyId, segmentIds } = req.body;
|
||||
|
||||
const hasFeatureStrategyPermission = this.accessService.hasPermission(
|
||||
req.user,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
projectId,
|
||||
environmentId,
|
||||
);
|
||||
|
||||
if (!hasFeatureStrategyPermission) {
|
||||
res.status(403).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if (segmentIds.length > this.config.strategySegmentsLimit) {
|
||||
throw new BadDataError(
|
||||
`Strategies may not have more than ${this.config.strategySegmentsLimit} segments`,
|
||||
);
|
||||
}
|
||||
|
||||
const segments = await this.segmentService.getByStrategy(strategyId);
|
||||
const currentSegmentIds = segments.map((segment) => segment.id);
|
||||
|
||||
await this.removeFromStrategy(
|
||||
strategyId,
|
||||
currentSegmentIds.filter((id) => !segmentIds.includes(id)),
|
||||
);
|
||||
|
||||
await this.addToStrategy(
|
||||
strategyId,
|
||||
segmentIds.filter((id) => !currentSegmentIds.includes(id)),
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
201,
|
||||
res,
|
||||
updateFeatureStrategySchema.$id,
|
||||
req.body,
|
||||
{
|
||||
location: `strategies/${strategyId}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getStrategiesBySegment(
|
||||
req: IAuthRequest<{ id: number }>,
|
||||
res: Response<SegmentStrategiesSchema>,
|
||||
): Promise<void> {
|
||||
const { id } = req.params;
|
||||
const strategies = await this.segmentService.getStrategies(id);
|
||||
|
||||
// Remove unnecessary IFeatureStrategy fields from the response.
|
||||
const segmentStrategies = strategies.map((strategy) => ({
|
||||
id: strategy.id,
|
||||
projectId: strategy.projectId,
|
||||
featureName: strategy.featureName,
|
||||
strategyName: strategy.strategyName,
|
||||
environment: strategy.environment,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
strategies: segmentStrategies,
|
||||
});
|
||||
}
|
||||
|
||||
async removeSegment(
|
||||
req: IAuthRequest<{ id: string }>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const id = Number(req.params.id);
|
||||
const strategies = await this.segmentService.getStrategies(id);
|
||||
|
||||
if (strategies.length > 0) {
|
||||
res.status(409).send();
|
||||
} else {
|
||||
await this.segmentService.delete(id, req.user);
|
||||
res.status(204).send();
|
||||
}
|
||||
}
|
||||
|
||||
async updateSegment(
|
||||
req: IAuthRequest<{ id: string }>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const id = Number(req.params.id);
|
||||
const updateRequest: UpsertSegmentSchema = {
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
project: req.body.project,
|
||||
constraints: req.body.constraints,
|
||||
};
|
||||
await this.segmentService.update(id, updateRequest, req.user);
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async getSegment(
|
||||
req: Request<{ id: string }>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const id = Number(req.params.id);
|
||||
const segment = await this.segmentService.get(id);
|
||||
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
||||
res.json(anonymiseKeys(segment, ['createdBy']));
|
||||
} else {
|
||||
res.json(segment);
|
||||
}
|
||||
}
|
||||
|
||||
async createSegment(
|
||||
req: IAuthRequest<any, any, UpsertSegmentSchema>,
|
||||
res: Response<AdminSegmentSchema>,
|
||||
): Promise<void> {
|
||||
const createRequest = req.body;
|
||||
const segment = await this.segmentService.create(
|
||||
createRequest,
|
||||
req.user,
|
||||
);
|
||||
this.openApiService.respondWithValidation(
|
||||
201,
|
||||
res,
|
||||
adminSegmentSchema.$id,
|
||||
serializeDates(segment),
|
||||
{ location: `segments/${segment.id}` },
|
||||
);
|
||||
}
|
||||
|
||||
async getSegments(
|
||||
req: IAuthRequest,
|
||||
res: Response<SegmentsSchema>,
|
||||
): Promise<void> {
|
||||
const segments = await this.segmentService.getAll();
|
||||
|
||||
const response = {
|
||||
segments: this.flagResolver.isEnabled('anonymiseEventLog')
|
||||
? anonymiseKeys(segments, ['createdBy'])
|
||||
: segments,
|
||||
};
|
||||
|
||||
this.openApiService.respondWithValidation<SegmentsSchema>(
|
||||
200,
|
||||
res,
|
||||
segmentsSchema.$id,
|
||||
serializeDates(response),
|
||||
);
|
||||
}
|
||||
|
||||
private async removeFromStrategy(
|
||||
strategyId: string,
|
||||
segmentIds: number[],
|
||||
): Promise<void> {
|
||||
await Promise.all(
|
||||
segmentIds.map((id) =>
|
||||
this.segmentService.removeFromStrategy(id, strategyId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async addToStrategy(
|
||||
strategyId: string,
|
||||
segmentIds: number[],
|
||||
): Promise<void> {
|
||||
await Promise.all(
|
||||
segmentIds.map((id) =>
|
||||
this.segmentService.addToStrategy(id, strategyId),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import {
|
||||
addonsSchema,
|
||||
addonTypeSchema,
|
||||
adminCountSchema,
|
||||
adminSegmentSchema,
|
||||
adminFeaturesQuerySchema,
|
||||
advancedPlaygroundRequestSchema,
|
||||
advancedPlaygroundResponseSchema,
|
||||
@ -158,6 +159,8 @@ import {
|
||||
createGroupSchema,
|
||||
doraFeaturesSchema,
|
||||
projectDoraMetricsSchema,
|
||||
segmentsSchema,
|
||||
updateFeatureStrategySegmentsSchema,
|
||||
dependentFeatureSchema,
|
||||
createDependentFeatureSchema,
|
||||
} from './spec';
|
||||
@ -177,6 +180,7 @@ import { createApplicationSchema } from './spec/create-application-schema';
|
||||
import { contextFieldStrategiesSchema } from './spec/context-field-strategies-schema';
|
||||
import { advancedPlaygroundEnvironmentFeatureSchema } from './spec/advanced-playground-environment-feature-schema';
|
||||
import { createFeatureNamingPatternSchema } from './spec/create-feature-naming-pattern-schema';
|
||||
import { segmentStrategiesSchema } from './spec/admin-strategies-schema';
|
||||
|
||||
// Schemas must have an $id property on the form "#/components/schemas/mySchema".
|
||||
export type SchemaId = typeof schemas[keyof typeof schemas]['$id'];
|
||||
@ -210,6 +214,8 @@ interface OpenAPIV3DocumentWithServers extends OpenAPIV3.Document {
|
||||
export const schemas: UnleashSchemas = {
|
||||
adminCountSchema,
|
||||
adminFeaturesQuerySchema,
|
||||
adminSegmentSchema,
|
||||
adminStrategiesSchema: segmentStrategiesSchema,
|
||||
addonParameterSchema,
|
||||
addonSchema,
|
||||
addonCreateUpdateSchema,
|
||||
@ -377,6 +383,8 @@ export const schemas: UnleashSchemas = {
|
||||
createFeatureNamingPatternSchema,
|
||||
doraFeaturesSchema,
|
||||
projectDoraMetricsSchema,
|
||||
segmentsSchema,
|
||||
updateFeatureStrategySegmentsSchema,
|
||||
dependentFeatureSchema,
|
||||
createDependentFeatureSchema,
|
||||
};
|
||||
|
@ -0,0 +1,54 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`updateEnvironmentSchema 1`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "additionalProperties",
|
||||
"message": "must NOT have additional properties",
|
||||
"params": {
|
||||
"additionalProperty": "additional",
|
||||
},
|
||||
"schemaPath": "#/additionalProperties",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/adminSegmentSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`updateEnvironmentSchema 2`] = `undefined`;
|
||||
|
||||
exports[`updateEnvironmentSchema 3`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'id'",
|
||||
"params": {
|
||||
"missingProperty": "id",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/adminSegmentSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`updateEnvironmentSchema 4`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "type",
|
||||
"message": "must be object",
|
||||
"params": {
|
||||
"type": "object",
|
||||
},
|
||||
"schemaPath": "#/type",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/adminSegmentSchema",
|
||||
}
|
||||
`;
|
@ -0,0 +1,69 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`segmentStrategiesSchema 1`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "type",
|
||||
"message": "must be object",
|
||||
"params": {
|
||||
"type": "object",
|
||||
},
|
||||
"schemaPath": "#/type",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/segmentStrategiesSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`segmentStrategiesSchema 2`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'strategies'",
|
||||
"params": {
|
||||
"missingProperty": "strategies",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/segmentStrategiesSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`segmentStrategiesSchema 3`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'strategies'",
|
||||
"params": {
|
||||
"missingProperty": "strategies",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/segmentStrategiesSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`segmentStrategiesSchema 4`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "/strategies/0",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'id'",
|
||||
"params": {
|
||||
"missingProperty": "id",
|
||||
},
|
||||
"schemaPath": "#/properties/strategies/items/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/segmentStrategiesSchema",
|
||||
}
|
||||
`;
|
@ -0,0 +1,20 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`updateEnvironmentSchema 1`] = `undefined`;
|
||||
|
||||
exports[`updateEnvironmentSchema 2`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "type",
|
||||
"message": "must be object",
|
||||
"params": {
|
||||
"type": "object",
|
||||
},
|
||||
"schemaPath": "#/type",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/segmentsSchema",
|
||||
}
|
||||
`;
|
@ -0,0 +1,35 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`updateFeatureStrategySegmentsSchema schema 1`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'projectId'",
|
||||
"params": {
|
||||
"missingProperty": "projectId",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/updateFeatureStrategySegmentsSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`updateFeatureStrategySegmentsSchema schema 2`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'projectId'",
|
||||
"params": {
|
||||
"missingProperty": "projectId",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/updateFeatureStrategySegmentsSchema",
|
||||
}
|
||||
`;
|
@ -0,0 +1,52 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`upsertSegmentSchema 1`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'constraints'",
|
||||
"params": {
|
||||
"missingProperty": "constraints",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/upsertSegmentSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`upsertSegmentSchema 2`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'name'",
|
||||
"params": {
|
||||
"missingProperty": "name",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/upsertSegmentSchema",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`upsertSegmentSchema 3`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"instancePath": "",
|
||||
"keyword": "required",
|
||||
"message": "must have required property 'name'",
|
||||
"params": {
|
||||
"missingProperty": "name",
|
||||
},
|
||||
"schemaPath": "#/required",
|
||||
},
|
||||
],
|
||||
"schema": "#/components/schemas/upsertSegmentSchema",
|
||||
}
|
||||
`;
|
59
src/lib/openapi/spec/admin-segment-schema.test.ts
Normal file
59
src/lib/openapi/spec/admin-segment-schema.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { AdminSegmentSchema } from './admin-segment-schema';
|
||||
|
||||
test('updateEnvironmentSchema', () => {
|
||||
const data: AdminSegmentSchema = {
|
||||
id: 1,
|
||||
name: 'release',
|
||||
constraints: [],
|
||||
createdAt: '2022-07-25 06:00:00',
|
||||
createdBy: 'test',
|
||||
description: 'a description',
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/adminSegmentSchema', data),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/adminSegmentSchema', {
|
||||
id: 1,
|
||||
name: 'release',
|
||||
constraints: [],
|
||||
createdAt: '2022-07-25 06:00:00',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/adminSegmentSchema', {
|
||||
id: 1,
|
||||
name: 'release',
|
||||
constraints: [],
|
||||
createdAt: '2022-07-25 06:00:00',
|
||||
additional: 'property',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/adminSegmentSchema', {
|
||||
id: 1,
|
||||
name: 'release',
|
||||
constraints: [],
|
||||
createdAt: 'wrong-format',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/adminSegmentSchema', {
|
||||
name: 'release',
|
||||
constraints: [],
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
|
||||
expect(
|
||||
validateSchema(
|
||||
'#/components/schemas/adminSegmentSchema',
|
||||
'not an object',
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
78
src/lib/openapi/spec/admin-segment-schema.ts
Normal file
78
src/lib/openapi/spec/admin-segment-schema.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { constraintSchema } from './constraint-schema';
|
||||
|
||||
export const adminSegmentSchema = {
|
||||
$id: '#/components/schemas/adminSegmentSchema',
|
||||
type: 'object',
|
||||
required: ['id', 'name', 'constraints', 'createdAt'],
|
||||
description:
|
||||
'A description of a [segment](https://docs.getunleash.io/reference/segments)',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: 'The ID of this segment',
|
||||
example: 2,
|
||||
minimum: 0,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The name of this segment',
|
||||
example: 'ios-users',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The description for this segment',
|
||||
example: 'IOS users segment',
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
description:
|
||||
'The list of constraints that are used in this segment',
|
||||
items: {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
usedInFeatures: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
description: 'The number of projects that use this segment',
|
||||
example: 3,
|
||||
nullable: true,
|
||||
},
|
||||
usedInProjects: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
description: 'The number of projects that use this segment',
|
||||
example: 2,
|
||||
nullable: true,
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
example: 'red-vista',
|
||||
description:
|
||||
'The project the segment belongs to. Only present if the segment is a project-specific segment.',
|
||||
},
|
||||
createdBy: {
|
||||
description: "The creator's email or username",
|
||||
example: 'someone@example.com',
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'When the segment was created',
|
||||
example: '2023-04-12T11:13:31.960Z',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
constraintSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type AdminSegmentSchema = FromSchema<typeof adminSegmentSchema>;
|
43
src/lib/openapi/spec/admin-strategies-schema.test.ts
Normal file
43
src/lib/openapi/spec/admin-strategies-schema.test.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { validateSchema } from '../validate';
|
||||
|
||||
test('segmentStrategiesSchema', () => {
|
||||
const validExamples = [
|
||||
{ strategies: [] },
|
||||
{
|
||||
strategies: [
|
||||
{
|
||||
id: 'test',
|
||||
projectId: '2',
|
||||
featureName: 'featureName',
|
||||
strategyName: 'strategyName',
|
||||
environment: 'environment',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
validExamples.forEach((obj) => {
|
||||
expect(
|
||||
validateSchema('#/components/schemas/segmentStrategiesSchema', obj),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
const invalidExamples = [
|
||||
'not an object',
|
||||
{},
|
||||
{ notStrategies: [] },
|
||||
{
|
||||
strategies: [
|
||||
{
|
||||
featureName: 'featureName',
|
||||
strategyName: 'strategyName',
|
||||
environment: 'environment',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
invalidExamples.forEach((obj) => {
|
||||
expect(
|
||||
validateSchema('#/components/schemas/segmentStrategiesSchema', obj),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
58
src/lib/openapi/spec/admin-strategies-schema.ts
Normal file
58
src/lib/openapi/spec/admin-strategies-schema.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const segmentStrategiesSchema = {
|
||||
$id: '#/components/schemas/segmentStrategiesSchema',
|
||||
type: 'object',
|
||||
required: ['strategies'],
|
||||
description: 'A collection of strategies belonging to a specified segment.',
|
||||
properties: {
|
||||
strategies: {
|
||||
description: 'The list of strategies',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'id',
|
||||
'featureName',
|
||||
'projectId',
|
||||
'environment',
|
||||
'strategyName',
|
||||
],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The ID of the strategy',
|
||||
example: 'e465c813-cffb-4232-b184-82b1d6fe9d3d',
|
||||
},
|
||||
featureName: {
|
||||
type: 'string',
|
||||
description: 'The ID of the strategy',
|
||||
example: 'new-signup-flow',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The ID of the project that the strategy belongs to.',
|
||||
example: 'red-vista',
|
||||
},
|
||||
environment: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The ID of the environment that the strategy belongs to.',
|
||||
example: 'development',
|
||||
},
|
||||
strategyName: {
|
||||
type: 'string',
|
||||
description: "The name of the strategy's type.",
|
||||
example: 'flexibleRollout',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type SegmentStrategiesSchema = FromSchema<
|
||||
typeof segmentStrategiesSchema
|
||||
>;
|
@ -1,7 +1,7 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const contextFieldStrategiesSchema = {
|
||||
$id: '#/components/schemas/segmentStrategiesSchema',
|
||||
$id: '#/components/schemas/contextFieldStrategiesSchema',
|
||||
type: 'object',
|
||||
description:
|
||||
'A wrapper object containing all strategies that use a specific context field',
|
||||
|
@ -157,5 +157,8 @@ export * from './create-group-schema';
|
||||
export * from './application-usage-schema';
|
||||
export * from './dora-features-schema';
|
||||
export * from './project-dora-metrics-schema';
|
||||
export * from './admin-segment-schema';
|
||||
export * from './segments-schema';
|
||||
export * from './update-feature-strategy-segments-schema';
|
||||
export * from './dependent-feature-schema';
|
||||
export * from './create-dependent-feature-schema';
|
||||
|
27
src/lib/openapi/spec/segments-schema.test.ts
Normal file
27
src/lib/openapi/spec/segments-schema.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { SegmentsSchema } from './segments-schema';
|
||||
|
||||
test('updateEnvironmentSchema', () => {
|
||||
const data: SegmentsSchema = {
|
||||
segments: [],
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/segmentsSchema', data),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/segmentsSchema', {
|
||||
segments: [],
|
||||
additional: 'property',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/segmentsSchema', {}),
|
||||
).toMatchSnapshot();
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/segmentsSchema', 'not an object'),
|
||||
).toMatchSnapshot();
|
||||
});
|
27
src/lib/openapi/spec/segments-schema.ts
Normal file
27
src/lib/openapi/spec/segments-schema.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { adminSegmentSchema } from './admin-segment-schema';
|
||||
import { constraintSchema } from './constraint-schema';
|
||||
|
||||
export const segmentsSchema = {
|
||||
$id: '#/components/schemas/segmentsSchema',
|
||||
description:
|
||||
'Data containing a list of [segments](https://docs.getunleash.io/reference/segments)',
|
||||
type: 'object',
|
||||
properties: {
|
||||
segments: {
|
||||
type: 'array',
|
||||
description: 'A list of segments',
|
||||
items: {
|
||||
$ref: '#/components/schemas/adminSegmentSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
adminSegmentSchema,
|
||||
constraintSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type SegmentsSchema = FromSchema<typeof segmentsSchema>;
|
@ -0,0 +1,36 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import { UpdateFeatureStrategySegmentsSchema } from './update-feature-strategy-segments-schema';
|
||||
|
||||
test('updateFeatureStrategySegmentsSchema schema', () => {
|
||||
const data: UpdateFeatureStrategySegmentsSchema = {
|
||||
strategyId: '1',
|
||||
segmentIds: [1, 2],
|
||||
projectId: 'default',
|
||||
environmentId: 'default',
|
||||
additional: 'property',
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema(
|
||||
'#/components/schemas/updateFeatureStrategySegmentsSchema',
|
||||
data,
|
||||
),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
validateSchema(
|
||||
'#/components/schemas/updateFeatureStrategySegmentsSchema',
|
||||
{},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
|
||||
expect(
|
||||
validateSchema(
|
||||
'#/components/schemas/updateFeatureStrategySegmentsSchema',
|
||||
{
|
||||
strategyId: '1',
|
||||
segmentIds: [],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
@ -0,0 +1,40 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const updateFeatureStrategySegmentsSchema = {
|
||||
$id: '#/components/schemas/updateFeatureStrategySegmentsSchema',
|
||||
type: 'object',
|
||||
required: ['projectId', 'strategyId', 'environmentId', 'segmentIds'],
|
||||
description: 'Data required to update segments for a strategy.',
|
||||
properties: {
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the project that the strategy belongs to.',
|
||||
example: 'red-vista',
|
||||
},
|
||||
strategyId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the strategy to update segments for.',
|
||||
example: '15d1e20b-6310-4e17-a0c2-9fb84de3053a',
|
||||
},
|
||||
environmentId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the strategy environment.',
|
||||
example: 'development',
|
||||
},
|
||||
segmentIds: {
|
||||
type: 'array',
|
||||
description:
|
||||
'The new list of segments (IDs) to use for this strategy. Any segments not in this list will be removed from the strategy.',
|
||||
example: [1, 5, 6],
|
||||
items: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type UpdateFeatureStrategySegmentsSchema = FromSchema<
|
||||
typeof updateFeatureStrategySegmentsSchema
|
||||
>;
|
44
src/lib/openapi/spec/upsert-segment-schema.test.ts
Normal file
44
src/lib/openapi/spec/upsert-segment-schema.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { validateSchema } from '../validate';
|
||||
|
||||
test('upsertSegmentSchema', () => {
|
||||
const validObjects = [
|
||||
{
|
||||
name: 'segment',
|
||||
constraints: [],
|
||||
},
|
||||
{
|
||||
name: 'segment',
|
||||
description: 'description',
|
||||
constraints: [],
|
||||
},
|
||||
{
|
||||
name: 'segment',
|
||||
description: 'description',
|
||||
constraints: [],
|
||||
additional: 'property',
|
||||
},
|
||||
];
|
||||
|
||||
validObjects.forEach((obj) =>
|
||||
expect(
|
||||
validateSchema('#/components/schemas/upsertSegmentSchema', obj),
|
||||
).toBeUndefined(),
|
||||
);
|
||||
|
||||
const invalidObjects = [
|
||||
{
|
||||
name: 'segment',
|
||||
},
|
||||
{
|
||||
description: 'description',
|
||||
constraints: [],
|
||||
},
|
||||
{},
|
||||
];
|
||||
|
||||
invalidObjects.forEach((obj) =>
|
||||
expect(
|
||||
validateSchema('#/components/schemas/upsertSegmentSchema', obj),
|
||||
).toMatchSnapshot(),
|
||||
);
|
||||
});
|
@ -4,46 +4,34 @@ import { constraintSchema } from './constraint-schema';
|
||||
export const upsertSegmentSchema = {
|
||||
$id: '#/components/schemas/upsertSegmentSchema',
|
||||
type: 'object',
|
||||
description:
|
||||
'Represents a segment of users defined by a set of constraints.',
|
||||
description: 'Data used to create or update a segment',
|
||||
required: ['name', 'constraints'],
|
||||
properties: {
|
||||
name: {
|
||||
description: 'The name of the segment',
|
||||
example: 'beta-users',
|
||||
type: 'string',
|
||||
description: 'The name of the segment.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The description of the segment.',
|
||||
description: 'A description of what the segment is for',
|
||||
example: 'Users willing to help us test and build new features.',
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description:
|
||||
'Project from where this segment will be accessible. If none is defined the segment will be global (i.e. accessible from any project).',
|
||||
description: 'The project the segment belongs to if any.',
|
||||
example: 'red-vista',
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
description:
|
||||
'List of constraints that determine which users will be part of the segment',
|
||||
description: 'The list of constraints that make up this segment',
|
||||
items: {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
example: {
|
||||
name: 'segment name',
|
||||
description: 'segment description',
|
||||
project: 'optional project id',
|
||||
constraints: [
|
||||
{
|
||||
contextName: 'environment',
|
||||
operator: 'IN',
|
||||
values: ['production', 'staging'],
|
||||
},
|
||||
],
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
constraintSchema,
|
||||
|
@ -32,6 +32,7 @@ import MaintenanceController from './maintenance';
|
||||
import { createKnexTransactionStarter } from '../../db/transaction';
|
||||
import { Db } from '../../db/db';
|
||||
import ExportImportController from '../../features/export-import-toggles/export-import-controller';
|
||||
import { SegmentsController } from '../../features/segment/segment-controller';
|
||||
|
||||
class AdminApi extends Controller {
|
||||
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
||||
@ -133,7 +134,10 @@ class AdminApi extends Controller {
|
||||
`/projects`,
|
||||
new FavoritesController(config, services).router,
|
||||
);
|
||||
|
||||
this.app.use(
|
||||
`/segments`,
|
||||
new SegmentsController(config, services).router,
|
||||
);
|
||||
this.app.use(
|
||||
'/maintenance',
|
||||
new MaintenanceController(config, services).router,
|
||||
|
424
src/test/e2e/api/admin/segment.e2e.test.ts
Normal file
424
src/test/e2e/api/admin/segment.e2e.test.ts
Normal file
@ -0,0 +1,424 @@
|
||||
import { randomId } from '../../../../lib/util/random-id';
|
||||
import {
|
||||
IFeatureStrategy,
|
||||
IFeatureToggleClient,
|
||||
ISegment,
|
||||
} from '../../../../lib/types/model';
|
||||
import { collectIds } from '../../../../lib/util/collect-ids';
|
||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import {
|
||||
addStrategyToFeatureEnv,
|
||||
createFeatureToggle,
|
||||
} from '../../helpers/app.utils';
|
||||
import {
|
||||
IUnleashTest,
|
||||
setupAppWithCustomConfig,
|
||||
} from '../../helpers/test-helper';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
const SEGMENTS_BASE_PATH = '/api/admin/segments';
|
||||
const FEATURES_LIST_BASE_PATH = '/api/admin/features';
|
||||
|
||||
// Recursively change all Date properties to string properties.
|
||||
type SerializeDatesDeep<T> = {
|
||||
[P in keyof T]: T[P] extends Date ? string : SerializeDatesDeep<T[P]>;
|
||||
};
|
||||
|
||||
const fetchSegments = (): Promise<SerializeDatesDeep<ISegment[]>> =>
|
||||
app.request
|
||||
.get(SEGMENTS_BASE_PATH)
|
||||
.expect(200)
|
||||
.then((res) => res.body.segments);
|
||||
|
||||
const fetchSegmentsByStrategy = (
|
||||
strategyId: string,
|
||||
): Promise<SerializeDatesDeep<ISegment[]>> =>
|
||||
app.request
|
||||
.get(`${SEGMENTS_BASE_PATH}/strategies/${strategyId}`)
|
||||
.expect(200)
|
||||
.then((res) => res.body.segments);
|
||||
|
||||
const fetchFeatures = (): Promise<IFeatureToggleClient[]> =>
|
||||
app.request
|
||||
.get(FEATURES_LIST_BASE_PATH)
|
||||
.expect(200)
|
||||
.then((res) => res.body.features);
|
||||
|
||||
const fetchSegmentStrategies = (
|
||||
segmentId: number,
|
||||
): Promise<IFeatureStrategy[]> =>
|
||||
app.request
|
||||
.get(`${SEGMENTS_BASE_PATH}/${segmentId}/strategies`)
|
||||
.expect(200)
|
||||
.then((res) => res.body.strategies);
|
||||
|
||||
const createSegment = (
|
||||
postData: object,
|
||||
expectStatusCode = 201,
|
||||
): Promise<unknown> =>
|
||||
app.request
|
||||
.post(SEGMENTS_BASE_PATH)
|
||||
.send(postData)
|
||||
.expect(expectStatusCode);
|
||||
|
||||
const updateSegment = (
|
||||
id: number,
|
||||
postData: object,
|
||||
expectStatusCode = 204,
|
||||
): Promise<unknown> =>
|
||||
app.request
|
||||
.put(`${SEGMENTS_BASE_PATH}/${id}`)
|
||||
.send(postData)
|
||||
.expect(expectStatusCode);
|
||||
|
||||
const addSegmentsToStrategy = (
|
||||
segmentIds: number[],
|
||||
strategyId: string,
|
||||
expectStatusCode = 201,
|
||||
): Promise<unknown> =>
|
||||
app.request
|
||||
.post(`${SEGMENTS_BASE_PATH}/strategies`)
|
||||
.set('Content-type', 'application/json')
|
||||
.send({
|
||||
strategyId,
|
||||
segmentIds,
|
||||
projectId: 'default',
|
||||
environmentId: 'default',
|
||||
additional: 'property',
|
||||
})
|
||||
.expect(expectStatusCode);
|
||||
|
||||
const mockFeatureToggle = () => ({
|
||||
name: randomId(),
|
||||
strategies: [{ name: 'flexibleRollout', constraints: [], parameters: {} }],
|
||||
});
|
||||
|
||||
const validateSegment = (
|
||||
postData: object,
|
||||
expectStatusCode = 204,
|
||||
): Promise<unknown> =>
|
||||
app.request
|
||||
.post(`${SEGMENTS_BASE_PATH}/validate`)
|
||||
.set('Content-type', 'application/json')
|
||||
.send(postData)
|
||||
.expect(expectStatusCode);
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('segments_api_serial', getLogger);
|
||||
app = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
anonymiseEventLog: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.stores.segmentStore.deleteAll();
|
||||
await db.stores.featureToggleStore.deleteAll();
|
||||
});
|
||||
|
||||
test('should validate segments', async () => {
|
||||
await createSegment({ something: 'a' }, 400);
|
||||
await createSegment({ name: randomId(), something: 'b' }, 400);
|
||||
await createSegment({ name: randomId(), constraints: 'b' }, 400);
|
||||
await createSegment({ constraints: [] }, 400);
|
||||
await createSegment({ name: randomId(), constraints: [{}] }, 400);
|
||||
await createSegment({ name: randomId(), constraints: [] });
|
||||
await createSegment({ name: randomId(), description: '', constraints: [] });
|
||||
});
|
||||
|
||||
test('should fail on missing properties', async () => {
|
||||
const res = await app.request
|
||||
.post(`${SEGMENTS_BASE_PATH}/strategies`)
|
||||
.set('Content-type', 'application/json')
|
||||
.send({
|
||||
projectId: 'default',
|
||||
environmentId: 'default',
|
||||
additional: 'property',
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('should create segments', async () => {
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
await createSegment({ name: 'c', constraints: [] });
|
||||
await createSegment({ name: 'b', constraints: [] });
|
||||
|
||||
const segments = await fetchSegments();
|
||||
expect(segments.map((s) => s.name)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
test('should update segments', async () => {
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
const [segmentA] = await fetchSegments();
|
||||
expect(segmentA.id).toBeGreaterThan(0);
|
||||
expect(segmentA.name).toEqual('a');
|
||||
expect(segmentA.createdAt).toBeDefined();
|
||||
expect(segmentA.constraints.length).toEqual(0);
|
||||
|
||||
await updateSegment(segmentA.id, { ...segmentA, name: 'b' });
|
||||
|
||||
const [segmentB] = await fetchSegments();
|
||||
expect(segmentB.id).toEqual(segmentA.id);
|
||||
expect(segmentB.name).toEqual('b');
|
||||
expect(segmentB.createdAt).toBeDefined();
|
||||
expect(segmentB.constraints.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should update segment constraints', async () => {
|
||||
const constraintA = { contextName: 'a', operator: 'IN', values: ['x'] };
|
||||
const constraintB = { contextName: 'b', operator: 'IN', values: ['y'] };
|
||||
await createSegment({ name: 'a', constraints: [constraintA] });
|
||||
const [segmentA] = await fetchSegments();
|
||||
expect(segmentA.constraints).toEqual([constraintA]);
|
||||
|
||||
await app.request
|
||||
.put(`${SEGMENTS_BASE_PATH}/${segmentA.id}`)
|
||||
.send({ ...segmentA, constraints: [constraintB, constraintA] })
|
||||
.expect(204);
|
||||
|
||||
const [segmentB] = await fetchSegments();
|
||||
expect(segmentB.constraints).toEqual([constraintB, constraintA]);
|
||||
});
|
||||
|
||||
test('should delete segments', async () => {
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
const segments = await fetchSegments();
|
||||
expect(segments.length).toEqual(1);
|
||||
|
||||
await app.request
|
||||
.delete(`${SEGMENTS_BASE_PATH}/${segments[0].id}`)
|
||||
.expect(204);
|
||||
|
||||
expect((await fetchSegments()).length).toEqual(0);
|
||||
});
|
||||
|
||||
test('should not delete segments used by strategies', async () => {
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
const toggle = mockFeatureToggle();
|
||||
await createFeatureToggle(app, toggle);
|
||||
const [segment] = await fetchSegments();
|
||||
|
||||
await addStrategyToFeatureEnv(
|
||||
app,
|
||||
{ ...toggle.strategies[0] },
|
||||
'default',
|
||||
toggle.name,
|
||||
);
|
||||
const [feature] = await fetchFeatures();
|
||||
//@ts-ignore
|
||||
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
|
||||
const segments = await fetchSegments();
|
||||
expect(segments.length).toEqual(1);
|
||||
|
||||
await app.request
|
||||
.delete(`${SEGMENTS_BASE_PATH}/${segments[0].id}`)
|
||||
.expect(409);
|
||||
|
||||
expect((await fetchSegments()).length).toEqual(1);
|
||||
});
|
||||
|
||||
test('should list strategies by segment', async () => {
|
||||
await createSegment({ name: 'S1', constraints: [] });
|
||||
await createSegment({ name: 'S2', constraints: [] });
|
||||
await createSegment({ name: 'S3', constraints: [] });
|
||||
const toggle1 = mockFeatureToggle();
|
||||
const toggle2 = mockFeatureToggle();
|
||||
const toggle3 = mockFeatureToggle();
|
||||
await createFeatureToggle(app, toggle1);
|
||||
await createFeatureToggle(app, toggle2);
|
||||
await createFeatureToggle(app, toggle3);
|
||||
|
||||
await addStrategyToFeatureEnv(
|
||||
app,
|
||||
{ ...toggle1.strategies[0] },
|
||||
'default',
|
||||
toggle1.name,
|
||||
);
|
||||
await addStrategyToFeatureEnv(
|
||||
app,
|
||||
{ ...toggle1.strategies[0] },
|
||||
'default',
|
||||
toggle2.name,
|
||||
);
|
||||
await addStrategyToFeatureEnv(
|
||||
app,
|
||||
{ ...toggle3.strategies[0] },
|
||||
'default',
|
||||
toggle3.name,
|
||||
);
|
||||
|
||||
const [feature1, feature2, feature3] = await fetchFeatures();
|
||||
const [segment1, segment2, segment3] = await fetchSegments();
|
||||
|
||||
await addSegmentsToStrategy(
|
||||
[segment1.id, segment2.id, segment3.id],
|
||||
//@ts-ignore
|
||||
feature1.strategies[0].id,
|
||||
);
|
||||
await addSegmentsToStrategy(
|
||||
[segment2.id, segment3.id],
|
||||
//@ts-ignore
|
||||
feature2.strategies[0].id,
|
||||
);
|
||||
//@ts-ignore
|
||||
await addSegmentsToStrategy([segment3.id], feature3.strategies[0].id);
|
||||
|
||||
const segmentStrategies1 = await fetchSegmentStrategies(segment1.id);
|
||||
const segmentStrategies2 = await fetchSegmentStrategies(segment2.id);
|
||||
const segmentStrategies3 = await fetchSegmentStrategies(segment3.id);
|
||||
|
||||
expect(collectIds(segmentStrategies1)).toEqual(
|
||||
collectIds(feature1.strategies),
|
||||
);
|
||||
|
||||
expect(collectIds(segmentStrategies2)).toEqual(
|
||||
collectIds([...feature1.strategies, ...feature2.strategies]),
|
||||
);
|
||||
|
||||
expect(collectIds(segmentStrategies3)).toEqual(
|
||||
collectIds([
|
||||
...feature1.strategies,
|
||||
...feature2.strategies,
|
||||
...feature3.strategies,
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should list segments by strategy', async () => {
|
||||
await createSegment({ name: 'S1', constraints: [] });
|
||||
await createSegment({ name: 'S2', constraints: [] });
|
||||
await createSegment({ name: 'S3', constraints: [] });
|
||||
const toggle1 = mockFeatureToggle();
|
||||
const toggle2 = mockFeatureToggle();
|
||||
const toggle3 = mockFeatureToggle();
|
||||
await createFeatureToggle(app, toggle1);
|
||||
await createFeatureToggle(app, toggle2);
|
||||
await createFeatureToggle(app, toggle3);
|
||||
|
||||
await addStrategyToFeatureEnv(
|
||||
app,
|
||||
{ ...toggle1.strategies[0] },
|
||||
'default',
|
||||
toggle1.name,
|
||||
);
|
||||
await addStrategyToFeatureEnv(
|
||||
app,
|
||||
{ ...toggle1.strategies[0] },
|
||||
'default',
|
||||
toggle2.name,
|
||||
);
|
||||
await addStrategyToFeatureEnv(
|
||||
app,
|
||||
{ ...toggle3.strategies[0] },
|
||||
'default',
|
||||
toggle3.name,
|
||||
);
|
||||
|
||||
const [feature1, feature2, feature3] = await fetchFeatures();
|
||||
const [segment1, segment2, segment3] = await fetchSegments();
|
||||
|
||||
await addSegmentsToStrategy(
|
||||
[segment1.id, segment2.id, segment3.id],
|
||||
//@ts-ignore
|
||||
feature1.strategies[0].id,
|
||||
);
|
||||
await addSegmentsToStrategy(
|
||||
[segment2.id, segment3.id],
|
||||
//@ts-ignore
|
||||
feature2.strategies[0].id,
|
||||
);
|
||||
//@ts-ignore
|
||||
await addSegmentsToStrategy([segment3.id], feature3.strategies[0].id);
|
||||
|
||||
const strategySegments1 = await fetchSegmentsByStrategy(
|
||||
//@ts-ignore
|
||||
feature1.strategies[0].id,
|
||||
);
|
||||
const strategySegments2 = await fetchSegmentsByStrategy(
|
||||
//@ts-ignore
|
||||
feature2.strategies[0].id,
|
||||
);
|
||||
const strategySegments3 = await fetchSegmentsByStrategy(
|
||||
//@ts-ignore
|
||||
feature3.strategies[0].id,
|
||||
);
|
||||
|
||||
expect(collectIds(strategySegments1)).toEqual(
|
||||
collectIds([segment1, segment2, segment3]),
|
||||
);
|
||||
|
||||
expect(collectIds(strategySegments2)).toEqual(
|
||||
collectIds([segment2, segment3]),
|
||||
);
|
||||
|
||||
expect(collectIds(strategySegments3)).toEqual(collectIds([segment3]));
|
||||
});
|
||||
|
||||
test('should reject duplicate segment names', async () => {
|
||||
await validateSegment({ name: 'a' });
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
await validateSegment({ name: 'a' }, 409);
|
||||
await validateSegment({ name: 'b' });
|
||||
});
|
||||
|
||||
test('should reject empty segment names', async () => {
|
||||
await validateSegment({ name: 'a' });
|
||||
await validateSegment({}, 400);
|
||||
await validateSegment({ name: '' }, 400);
|
||||
});
|
||||
|
||||
test('should reject duplicate segment names on create', async () => {
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
await createSegment({ name: 'a', constraints: [] }, 409);
|
||||
await validateSegment({ name: 'b' });
|
||||
});
|
||||
|
||||
test('should reject duplicate segment names on update', async () => {
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
await createSegment({ name: 'b', constraints: [] });
|
||||
const [segmentA, segmentB] = await fetchSegments();
|
||||
await updateSegment(segmentA.id, { name: 'b', constraints: [] }, 409);
|
||||
await updateSegment(segmentB.id, { name: 'a', constraints: [] }, 409);
|
||||
await updateSegment(segmentA.id, { name: 'a', constraints: [] });
|
||||
await updateSegment(segmentA.id, { name: 'c', constraints: [] });
|
||||
});
|
||||
|
||||
test('Should anonymise createdBy field if anonymiseEventLog flag is set', async () => {
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
await createSegment({ name: 'b', constraints: [] });
|
||||
const segments = await fetchSegments();
|
||||
expect(segments).toHaveLength(2);
|
||||
expect(segments[0].createdBy).toContain('unleash.run');
|
||||
});
|
||||
|
||||
test('Should show usage in features and projects', async () => {
|
||||
await createSegment({ name: 'a', constraints: [] });
|
||||
const toggle = mockFeatureToggle();
|
||||
await createFeatureToggle(app, toggle);
|
||||
const [segment] = await fetchSegments();
|
||||
await addStrategyToFeatureEnv(
|
||||
app,
|
||||
{ ...toggle.strategies[0] },
|
||||
'default',
|
||||
toggle.name,
|
||||
);
|
||||
const [feature] = await fetchFeatures();
|
||||
//@ts-ignore
|
||||
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
|
||||
|
||||
const segments = await fetchSegments();
|
||||
expect(segments).toMatchObject([{ usedInFeatures: 1, usedInProjects: 1 }]);
|
||||
});
|
26
src/test/e2e/helpers/app.utils.ts
Normal file
26
src/test/e2e/helpers/app.utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { CreateFeatureStrategySchema } from 'lib/openapi';
|
||||
import { IUnleashTest } from './test-helper';
|
||||
|
||||
export const FEATURES_BASE_PATH = '/api/admin/projects/default/features';
|
||||
export const ADMIN_BASE_PATH = '/api/admin';
|
||||
|
||||
export const createFeatureToggle = (
|
||||
app: IUnleashTest,
|
||||
postData: object,
|
||||
expectStatusCode = 201,
|
||||
): Promise<unknown> =>
|
||||
app.request
|
||||
.post(FEATURES_BASE_PATH)
|
||||
.send(postData)
|
||||
.expect(expectStatusCode);
|
||||
|
||||
export const addStrategyToFeatureEnv = (
|
||||
app: IUnleashTest,
|
||||
postData: CreateFeatureStrategySchema,
|
||||
envName: string,
|
||||
featureName: string,
|
||||
expectStatusCode = 200,
|
||||
): Promise<any> => {
|
||||
const url = `${ADMIN_BASE_PATH}/projects/default/features/${featureName}/environments/${envName}/strategies`;
|
||||
return app.request.post(url).send(postData).expect(expectStatusCode);
|
||||
};
|
@ -6,7 +6,8 @@ import VideoContent from '@site/src/components/VideoContent.jsx'
|
||||
|
||||
:::info Availability
|
||||
|
||||
Segments are available to Unleash Pro and Unleash Enterprise users since **Unleash 4.13**.
|
||||
Segments are available to Unleash Pro and Unleash Enterprise users since **Unleash 4.13** and
|
||||
was made available for Open Source from Unleash v5.5.
|
||||
|
||||
:::
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user