diff --git a/src/lib/routes/admin-api/context.ts b/src/lib/routes/admin-api/context.ts index 328eb71684..04b82ad85a 100644 --- a/src/lib/routes/admin-api/context.ts +++ b/src/lib/routes/admin-api/context.ts @@ -14,6 +14,7 @@ import { IUnleashServices } from '../../types/services'; import ContextService from '../../services/context-service'; import { Logger } from '../../logger'; import { IAuthRequest } from '../unleash-types'; +import { IConstraint } from 'lib/types/model'; class ContextController extends Controller { private logger: Logger; diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index a677a1c01e..afcdf4d2a7 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -337,6 +337,13 @@ export default class ProjectFeaturesController extends Controller { res.status(200).json(updatedStrategy); } + async validateConstraint(req: Request, res: Response): Promise { + const constraint: IConstraint = { ...req.body }; + + await this.featureService.validateConstraint(constraint); + res.status(204).send(); + } + async getStrategy( req: IAuthRequest, res: Response, diff --git a/src/lib/schema/constraint-value-types.test.ts b/src/lib/schema/constraint-value-types.test.ts new file mode 100644 index 0000000000..ba27913abf --- /dev/null +++ b/src/lib/schema/constraint-value-types.test.ts @@ -0,0 +1,25 @@ +import { constraintNumberTypeSchema } from './constraint-value-types'; + +test('should require number', async () => { + try { + await constraintNumberTypeSchema.validateAsync('test'); + } catch (error) { + expect(error.details[0].message).toEqual('"value" must be a number'); + } +}); + +test('should allow strings that can be parsed to a number', async () => { + await constraintNumberTypeSchema.validateAsync('5'); +}); + +test('should allow floating point numbers', async () => { + await constraintNumberTypeSchema.validateAsync(5.72); +}); + +test('should allow numbers', async () => { + await constraintNumberTypeSchema.validateAsync(5); +}); + +test('should allow negative numbers', async () => { + await constraintNumberTypeSchema.validateAsync(-5); +}); diff --git a/src/lib/schema/constraint-value-types.ts b/src/lib/schema/constraint-value-types.ts new file mode 100644 index 0000000000..8470f331a0 --- /dev/null +++ b/src/lib/schema/constraint-value-types.ts @@ -0,0 +1,3 @@ +import joi from 'joi'; + +export const constraintNumberTypeSchema = joi.number(); diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index d237c87c62..44b15d0d40 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -39,6 +39,7 @@ import { FeatureToggleDTO, FeatureToggleLegacy, FeatureToggleWithEnvironment, + IConstraint, IEnvironmentDetail, IFeatureEnvironmentInfo, IFeatureOverview, @@ -50,7 +51,13 @@ import { } from '../types/model'; import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store'; -import { DEFAULT_ENV } from '../util/constants'; +import { + DATE_OPERATORS, + DEFAULT_ENV, + NUM_OPERATORS, + SEMVER_OPERATORS, + STRING_OPERATORS, +} from '../util/constants'; import { applyPatch, deepClone, Operation } from 'fast-json-patch'; import { OperationDeniedError } from '../error/operation-denied-error'; @@ -63,6 +70,10 @@ interface IFeatureStrategyContext extends IFeatureContext { environment: string; } +const oneOf = (values: string[], match: string) => { + return values.some((value) => value === match); +}; + class FeatureToggleService { private logger: Logger; @@ -140,6 +151,25 @@ class FeatureToggleService { } } + validateConstraint(constraint: IConstraint): void { + const { operator } = constraint; + if (oneOf(NUM_OPERATORS, operator)) { + // Validate number value + } + + if (oneOf(STRING_OPERATORS, operator)) { + // validate string values array + } + + if (oneOf(SEMVER_OPERATORS, operator)) { + // validate semver + } + + if (oneOf(DATE_OPERATORS, operator)) { + // validate dates + } + } + async patchFeature( project: string, featureName: string, diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 57641040a1..301b0df1c0 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -6,7 +6,10 @@ import { IUser } from './user'; export interface IConstraint { contextName: string; operator: string; - values: string[]; + values?: string[]; + value?: string; + inverted?: boolean; + caseInsensitive?: boolean; } export enum WeightType { VARIABLE = 'variable', diff --git a/src/lib/util/constants.ts b/src/lib/util/constants.ts index 45b0ab23b8..6b95bc9e76 100644 --- a/src/lib/util/constants.ts +++ b/src/lib/util/constants.ts @@ -5,3 +5,50 @@ export const ENVIRONMENT_PERMISSION_TYPE = 'environment'; export const PROJECT_PERMISSION_TYPE = 'project'; export const CUSTOM_ROLE_TYPE = 'custom'; + +/* CONTEXT FIELD OPERATORS */ + +export const NOT_IN = 'NOT_IN'; +export const IN = 'IN'; +export const STR_ENDS_WITH = 'STR_ENDS_WITH'; +export const STR_STARTS_WITH = 'STR_STARTS_WITH'; +export const STR_CONTAINS = 'STR_CONTAINS'; +export const NUM_EQ = 'NUM_EQ'; +export const NUM_GT = 'NUM_GT'; +export const NUM_GTE = 'NUM_GTE'; +export const NUM_LT = 'NUM_LT'; +export const NUM_LTE = 'NUM_LTE'; +export const DATE_AFTER = 'DATE_AFTER'; +export const DATE_BEFORE = 'DATE_BEFORE'; +export const SEMVER_EQ = 'SEMVER_EQ'; +export const SEMVER_GT = 'SEMVER_GT'; +export const SEMVER_LT = 'SEMVER_LT'; + +export const ALL_OPERATORS = [ + NOT_IN, + IN, + STR_ENDS_WITH, + STR_STARTS_WITH, + STR_CONTAINS, + NUM_EQ, + NUM_GT, + NUM_GTE, + NUM_LT, + NUM_LTE, + DATE_AFTER, + DATE_BEFORE, + SEMVER_EQ, + SEMVER_GT, + SEMVER_LT, +]; + +export const STRING_OPERATORS = [ + STR_ENDS_WITH, + STR_STARTS_WITH, + STR_CONTAINS, + IN, + NOT_IN, +]; +export const NUM_OPERATORS = [NUM_EQ, NUM_GT, NUM_GTE, NUM_LT, NUM_LTE]; +export const DATE_OPERATORS = [DATE_AFTER, DATE_BEFORE]; +export const SEMVER_OPERATORS = [SEMVER_EQ, SEMVER_GT, SEMVER_LT]; diff --git a/src/lib/util/validators/constraint-types.ts b/src/lib/util/validators/constraint-types.ts new file mode 100644 index 0000000000..ebeccc13de --- /dev/null +++ b/src/lib/util/validators/constraint-types.ts @@ -0,0 +1,5 @@ +import { constraintNumberTypeSchema } from 'lib/schema/constraint-value-types'; + +export const validateNumber = async (value: unknown): Promise => { + await constraintNumberTypeSchema.validateAsync(value); +}; diff --git a/src/server-dev.ts b/src/server-dev.ts index f408dfaec2..b8c1a781eb 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -12,7 +12,7 @@ process.nextTick(async () => { password: 'passord', host: 'localhost', port: 5432, - database: 'unleash', + database: 'contextfields', ssl: false, }, server: {