1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-27 13:49:10 +02:00

Enforce [constraintId] on incoming constraints

This commit is contained in:
Thomas Heartman 2025-07-18 13:52:19 +02:00
parent 71c9a53380
commit 872c582eb9
No known key found for this signature in database
GPG Key ID: BD1F880DAED1EE78
5 changed files with 116 additions and 8 deletions

View File

@ -9,6 +9,7 @@ import {
getSegmentChangesThatWouldBeOverwritten,
getStrategyChangesThatWouldBeOverwritten,
} from './strategy-change-diff-calculation.js';
import { constraintId } from 'constants/constraintId.js';
describe('Strategy change conflict detection', () => {
const existingStrategy: IFeatureStrategy = {
@ -175,6 +176,7 @@ describe('Strategy change conflict detection', () => {
operator: 'IN' as const,
contextName: 'appName',
caseInsensitive: false,
[constraintId]: 'id1',
},
],
variants: [
@ -230,6 +232,7 @@ describe('Strategy change conflict detection', () => {
operator: 'IN' as const,
contextName: 'appName',
caseInsensitive: false,
[constraintId]: 'id2',
},
],
};
@ -249,6 +252,7 @@ describe('Strategy change conflict detection', () => {
inverted: false,
operator: 'IN' as const,
values: ['blah'],
[constraintId]: 'id2',
},
],
},
@ -478,6 +482,7 @@ describe('Segment change conflict detection', () => {
operator: 'IN' as const,
contextName: 'appName',
caseInsensitive: false,
[constraintId]: 'id3',
},
],
};
@ -494,6 +499,7 @@ describe('Segment change conflict detection', () => {
operator: 'IN' as const,
contextName: 'appName',
caseInsensitive: false,
[constraintId]: 'id4',
},
],
},

View File

@ -11,6 +11,8 @@ import {
} from '@server/types/permissions';
import { StrategyItem } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem';
import type { IFeatureStrategy } from 'interfaces/strategy';
import { constraintId } from 'constants/constraintId';
import { v4 as uuidv4 } from 'uuid';
interface ProjectEnvironmentDefaultStrategyProps {
environment: ProjectEnvironmentType;
@ -55,7 +57,11 @@ export const ProjectEnvironmentDefaultStrategy = ({
return {
...baseDefaultStrategy,
disabled: false,
constraints: baseDefaultStrategy.constraints ?? [],
constraints:
baseDefaultStrategy.constraints?.map((constraint) => ({
...constraint,
[constraintId]: uuidv4(),
})) ?? [],
title: baseDefaultStrategy.title ?? '',
parameters: baseDefaultStrategy.parameters ?? {},
};

View File

@ -1,7 +1,22 @@
import useSWR from 'swr';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler.js';
import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types';
import type {
ChangeRequestType,
IChangeRequestAddStrategy,
IChangeRequestUpdateStrategy,
IFeatureChange,
} from 'component/changeRequest/changeRequest.types';
import { useMemo } from 'react';
import { constraintId } from 'constants/constraintId.js';
import { v4 as uuidv4 } from 'uuid';
const isAddStrategyChange = (
change: IFeatureChange,
): change is IChangeRequestAddStrategy => change.action === 'addStrategy';
const isUpdateStrategyChange = (
change: IFeatureChange,
): change is IChangeRequestUpdateStrategy => change.action === 'updateStrategy';
export const useChangeRequest = (projectId: string, id: string) => {
const { data, error, mutate } = useSWR<ChangeRequestType>(
@ -10,8 +25,47 @@ export const useChangeRequest = (projectId: string, id: string) => {
{ refreshInterval: 15000 },
);
const dataWithConstraintIds: ChangeRequestType | undefined = useMemo(() => {
if (!data) {
return data;
}
const features = data.features.map((feature) => {
const changes: IFeatureChange[] = feature.changes.map((change) => {
if (
isAddStrategyChange(change) ||
isUpdateStrategyChange(change)
) {
const { constraints, ...rest } = change.payload;
return {
...change,
payload: {
...rest,
constraints: constraints.map((constraint) => ({
...constraint,
[constraintId]: uuidv4(),
})),
},
} as IFeatureChange;
}
return change;
});
return {
...feature,
changes,
};
});
const value: ChangeRequestType = {
...data,
features,
};
return value;
}, [data]);
return {
data,
data: dataWithConstraintIds,
loading: !error && !data,
refetchChangeRequest: () => mutate(),
error,

View File

@ -1,9 +1,12 @@
import useSWR, { type SWRConfiguration } from 'swr';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { emptyFeature } from './emptyFeature.ts';
import handleErrorResponses from '../httpErrorResponseHandler.ts';
import { formatApiPath } from 'utils/formatPath';
import type { IFeatureToggle } from 'interfaces/featureToggle';
import { constraintId } from 'constants/constraintId.ts';
import { v4 as uuidv4 } from 'uuid';
import type { IFeatureStrategy } from 'interfaces/strategy.ts';
export interface IUseFeatureOutput {
feature: IFeatureToggle;
@ -35,8 +38,10 @@ export const useFeature = (
mutate().catch(console.warn);
}, [mutate]);
const feature = useMemo(enrichConstraintsWithIds(data), [data?.body]);
return {
feature: data?.body || emptyFeature,
feature,
refetchFeature,
loading: !error && !data,
status: data?.status,
@ -63,6 +68,41 @@ export const featureFetcher = async (
};
};
export const enrichConstraintsWithIds = (data?: IFeatureResponse) => () => {
if (!data?.body) {
return emptyFeature;
}
const { strategies, environments, ...rest } = data.body;
const addConstraintIds = (strategy: IFeatureStrategy) => {
const { constraints, ...strategyRest } = strategy;
return {
...strategyRest,
constraints: constraints?.map((constraint) => ({
...constraint,
[constraintId]: uuidv4(),
})),
};
};
const strategiesWithConstraintIds = strategies?.map(addConstraintIds);
const environmentsWithStrategyIds = environments?.map((environment) => {
const { strategies, ...environmentRest } = environment;
return {
...environmentRest,
strategies: strategies?.map(addConstraintIds),
};
});
return {
...rest,
strategies: strategiesWithConstraintIds,
environments: environmentsWithStrategyIds,
};
};
export const formatFeatureApiPath = (
projectId: string,
featureId: string,

View File

@ -1,12 +1,12 @@
import useSWRImmutable from 'swr/immutable';
import { useCallback } from 'react';
import { emptyFeature } from './emptyFeature.js';
import { useCallback, useMemo } from 'react';
import {
type IUseFeatureOutput,
type IFeatureResponse,
featureFetcher,
formatFeatureApiPath,
useFeature,
enrichConstraintsWithIds,
} from 'hooks/api/getters/useFeature/useFeature';
// useFeatureImmutable is like useFeature, except it won't refetch data on
@ -29,8 +29,10 @@ export const useFeatureImmutable = (
await refetchFeature();
}, [mutate, refetchFeature]);
const feature = useMemo(enrichConstraintsWithIds(data), [data?.body]);
return {
feature: data?.body || emptyFeature,
feature,
refetchFeature: refetch,
loading: !error && !data,
status: data?.status,