mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: always add values to constraints (#1448)
This commit is contained in:
		
							parent
							
								
									957ec1c16d
								
							
						
					
					
						commit
						1cc43c0a4a
					
				@ -2,6 +2,9 @@ import joi from 'joi';
 | 
			
		||||
 | 
			
		||||
export const constraintNumberTypeSchema = joi.number();
 | 
			
		||||
 | 
			
		||||
export const constraintStringTypeSchema = joi.array().items(joi.string());
 | 
			
		||||
export const constraintStringTypeSchema = joi
 | 
			
		||||
    .array()
 | 
			
		||||
    .items(joi.string())
 | 
			
		||||
    .min(1);
 | 
			
		||||
 | 
			
		||||
export const constraintDateTypeSchema = joi.date();
 | 
			
		||||
 | 
			
		||||
@ -210,7 +210,7 @@ test('should not accept empty constraint values', () => {
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should not accept empty list of constraint values', () => {
 | 
			
		||||
test('should accept empty list of constraint values', async () => {
 | 
			
		||||
    const toggle = {
 | 
			
		||||
        name: 'app.constraints.empty.value.list',
 | 
			
		||||
        type: 'release',
 | 
			
		||||
@ -231,10 +231,10 @@ test('should not accept empty list of constraint values', () => {
 | 
			
		||||
        ],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const { error } = featureSchema.validate(toggle);
 | 
			
		||||
    expect(error.details[0].message).toEqual(
 | 
			
		||||
        '"strategies[0].constraints[0].values" must contain at least 1 items',
 | 
			
		||||
    );
 | 
			
		||||
    const validated = await featureSchema.validateAsync(toggle);
 | 
			
		||||
    expect(validated.strategies.length).toEqual(1);
 | 
			
		||||
    expect(validated.strategies[0].constraints.length).toEqual(1);
 | 
			
		||||
    expect(validated.strategies[0].constraints[0].values).toEqual([]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Filter queries should accept a list of tag values', () => {
 | 
			
		||||
@ -289,3 +289,18 @@ test('constraint schema should only allow specified operators', async () => {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('constraint schema should add a values array by default', async () => {
 | 
			
		||||
    const validated = await constraintSchema.validateAsync({
 | 
			
		||||
        contextName: 'x',
 | 
			
		||||
        operator: 'NUM_EQ',
 | 
			
		||||
        value: 1,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(validated).toEqual({
 | 
			
		||||
        contextName: 'x',
 | 
			
		||||
        operator: 'NUM_EQ',
 | 
			
		||||
        value: 1,
 | 
			
		||||
        values: [],
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,8 @@ export const nameSchema = joi
 | 
			
		||||
export const constraintSchema = joi.object().keys({
 | 
			
		||||
    contextName: joi.string(),
 | 
			
		||||
    operator: joi.string().valid(...ALL_OPERATORS),
 | 
			
		||||
    values: joi.array().items(joi.string().min(1).max(100)).min(1).optional(),
 | 
			
		||||
    // Constraints must have a values array to support legacy SDKs.
 | 
			
		||||
    values: joi.array().items(joi.string().min(1).max(100)).default([]),
 | 
			
		||||
    value: joi.optional(),
 | 
			
		||||
    caseInsensitive: joi.boolean().optional(),
 | 
			
		||||
    inverted: joi.boolean().optional(),
 | 
			
		||||
 | 
			
		||||
@ -165,17 +165,19 @@ class FeatureToggleService {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async validateConstraints(constraints: IConstraint[]): Promise<void> {
 | 
			
		||||
    async validateConstraints(
 | 
			
		||||
        constraints: IConstraint[],
 | 
			
		||||
    ): Promise<IConstraint[]> {
 | 
			
		||||
        const validations = constraints.map((constraint) => {
 | 
			
		||||
            return this.validateConstraint(constraint);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await Promise.all(validations);
 | 
			
		||||
        return Promise.all(validations);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async validateConstraint(constraint: IConstraint): Promise<void> {
 | 
			
		||||
    async validateConstraint(input: IConstraint): Promise<IConstraint> {
 | 
			
		||||
        const constraint = await constraintSchema.validateAsync(input);
 | 
			
		||||
        const { operator } = constraint;
 | 
			
		||||
        await constraintSchema.validateAsync(constraint);
 | 
			
		||||
        const contextDefinition = await this.contextFieldStore.get(
 | 
			
		||||
            constraint.contextName,
 | 
			
		||||
        );
 | 
			
		||||
@ -218,6 +220,8 @@ class FeatureToggleService {
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return constraint;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async patchFeature(
 | 
			
		||||
@ -282,7 +286,9 @@ class FeatureToggleService {
 | 
			
		||||
        await this.validateFeatureContext(context);
 | 
			
		||||
 | 
			
		||||
        if (strategyConfig.constraints?.length > 0) {
 | 
			
		||||
            await this.validateConstraints(strategyConfig.constraints);
 | 
			
		||||
            strategyConfig.constraints = await this.validateConstraints(
 | 
			
		||||
                strategyConfig.constraints,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
@ -342,15 +348,17 @@ class FeatureToggleService {
 | 
			
		||||
        this.validateFeatureStrategyContext(existingStrategy, context);
 | 
			
		||||
 | 
			
		||||
        if (existingStrategy.id === id) {
 | 
			
		||||
            if (updates.constraints?.length > 0) {
 | 
			
		||||
                updates.constraints = await this.validateConstraints(
 | 
			
		||||
                    updates.constraints,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const strategy = await this.featureStrategiesStore.updateStrategy(
 | 
			
		||||
                id,
 | 
			
		||||
                updates,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (updates.constraints?.length > 0) {
 | 
			
		||||
                await this.validateConstraints(updates.constraints);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Store event!
 | 
			
		||||
            const tags = await this.tagStore.getAllTagsForFeature(featureName);
 | 
			
		||||
            const data = this.featureStrategyToPublic(strategy);
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,8 @@ import ApiUser from '../../../../../lib/types/api-user';
 | 
			
		||||
import { ApiTokenType } from '../../../../../lib/types/models/api-token';
 | 
			
		||||
import IncompatibleProjectError from '../../../../../lib/error/incompatible-project-error';
 | 
			
		||||
import { IVariant, WeightType } from '../../../../../lib/types/model';
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
import supertest from 'supertest';
 | 
			
		||||
 | 
			
		||||
let app: IUnleashTest;
 | 
			
		||||
let db: ITestDb;
 | 
			
		||||
@ -2066,3 +2068,108 @@ test('Can create toggle with impression data on different project', async () =>
 | 
			
		||||
            expect(res.body.impressionData).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should reject invalid constraint values for multi-valued constraints', async () => {
 | 
			
		||||
    const project = await db.stores.projectStore.create({
 | 
			
		||||
        id: uuidv4(),
 | 
			
		||||
        name: uuidv4(),
 | 
			
		||||
        description: '',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const toggle = await db.stores.featureToggleStore.create(project.id, {
 | 
			
		||||
        name: uuidv4(),
 | 
			
		||||
        impressionData: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const mockStrategy = (values: string[]) => ({
 | 
			
		||||
        name: uuidv4(),
 | 
			
		||||
        constraints: [{ contextName: 'userId', operator: 'IN', values }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const featureStrategiesPath = `/api/admin/projects/${project.id}/features/${toggle.name}/environments/default/strategies`;
 | 
			
		||||
 | 
			
		||||
    await app.request
 | 
			
		||||
        .post(featureStrategiesPath)
 | 
			
		||||
        .send(mockStrategy([]))
 | 
			
		||||
        .expect(400);
 | 
			
		||||
    await app.request
 | 
			
		||||
        .post(featureStrategiesPath)
 | 
			
		||||
        .send(mockStrategy(['']))
 | 
			
		||||
        .expect(400);
 | 
			
		||||
    const { body: strategy } = await app.request
 | 
			
		||||
        .post(featureStrategiesPath)
 | 
			
		||||
        .send(mockStrategy(['a']))
 | 
			
		||||
        .expect(200);
 | 
			
		||||
 | 
			
		||||
    await app.request
 | 
			
		||||
        .put(`${featureStrategiesPath}/${strategy.id}`)
 | 
			
		||||
        .send(mockStrategy([]))
 | 
			
		||||
        .expect(400);
 | 
			
		||||
    await app.request
 | 
			
		||||
        .put(`${featureStrategiesPath}/${strategy.id}`)
 | 
			
		||||
        .send(mockStrategy(['']))
 | 
			
		||||
        .expect(400);
 | 
			
		||||
    await app.request
 | 
			
		||||
        .put(`${featureStrategiesPath}/${strategy.id}`)
 | 
			
		||||
        .send(mockStrategy(['a']))
 | 
			
		||||
        .expect(200);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should add default constraint values for single-valued constraints', async () => {
 | 
			
		||||
    const project = await db.stores.projectStore.create({
 | 
			
		||||
        id: uuidv4(),
 | 
			
		||||
        name: uuidv4(),
 | 
			
		||||
        description: '',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const toggle = await db.stores.featureToggleStore.create(project.id, {
 | 
			
		||||
        name: uuidv4(),
 | 
			
		||||
        impressionData: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const constraintValue = {
 | 
			
		||||
        contextName: 'userId',
 | 
			
		||||
        operator: 'NUM_EQ',
 | 
			
		||||
        value: '1',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const constraintValues = {
 | 
			
		||||
        contextName: 'userId',
 | 
			
		||||
        operator: 'IN',
 | 
			
		||||
        values: ['a', 'b', 'c'],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const mockStrategy = (constraint: unknown) => ({
 | 
			
		||||
        name: uuidv4(),
 | 
			
		||||
        constraints: [constraint],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const expectValues = (res: supertest.Response, values: unknown[]) => {
 | 
			
		||||
        expect(res.body.constraints.length).toEqual(1);
 | 
			
		||||
        expect(res.body.constraints[0].values).toEqual(values);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const featureStrategiesPath = `/api/admin/projects/${project.id}/features/${toggle.name}/environments/default/strategies`;
 | 
			
		||||
 | 
			
		||||
    await app.request
 | 
			
		||||
        .post(featureStrategiesPath)
 | 
			
		||||
        .send(mockStrategy(constraintValue))
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect((res) => expectValues(res, []));
 | 
			
		||||
    const { body: strategy } = await app.request
 | 
			
		||||
        .post(featureStrategiesPath)
 | 
			
		||||
        .send(mockStrategy(constraintValues))
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect((res) => expectValues(res, constraintValues.values));
 | 
			
		||||
 | 
			
		||||
    await app.request
 | 
			
		||||
        .put(`${featureStrategiesPath}/${strategy.id}`)
 | 
			
		||||
        .send(mockStrategy(constraintValue))
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect((res) => expectValues(res, []));
 | 
			
		||||
    await app.request
 | 
			
		||||
        .put(`${featureStrategiesPath}/${strategy.id}`)
 | 
			
		||||
        .send(mockStrategy(constraintValues))
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect((res) => expectValues(res, constraintValues.values));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user