From a0862cfc1065db211c9c6eb66ccd8a0b0a6f9dba Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 20 Jun 2023 14:28:02 +0200 Subject: [PATCH] feat: Query complexity validation (#4017) --- .../Playground/AdvancedPlayground.tsx | 36 ++++++++++++++--- .../features/playground/playground-service.ts | 7 ++++ .../validateQueryComplexity.test.ts | 39 +++++++++++++++++++ .../playground/validateQueryComplexity.ts | 19 +++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 src/lib/features/playground/validateQueryComplexity.test.ts create mode 100644 src/lib/features/playground/validateQueryComplexity.ts diff --git a/frontend/src/component/playground/Playground/AdvancedPlayground.tsx b/frontend/src/component/playground/Playground/AdvancedPlayground.tsx index 963f4472b7..23f94e5743 100644 --- a/frontend/src/component/playground/Playground/AdvancedPlayground.tsx +++ b/frontend/src/component/playground/Playground/AdvancedPlayground.tsx @@ -1,6 +1,6 @@ import { FormEventHandler, useEffect, useState, VFC } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { Box, Paper, useTheme } from '@mui/material'; +import { Box, Paper, useTheme, styled, Alert } from '@mui/material'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import useToast from 'hooks/useToast'; @@ -21,6 +21,11 @@ import Loader from '../../common/Loader/Loader'; import { AdvancedPlaygroundResultsTable } from './AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable'; import { AdvancedPlaygroundResponseSchema } from 'openapi'; import { createLocalStorage } from 'utils/createLocalStorage'; +import { BadRequestError } from '../../../utils/apiUtils'; + +const StyledAlert = styled(Alert)(({ theme }) => ({ + marginBottom: theme.spacing(3), +})); export const AdvancedPlayground: VFC<{ FormComponent?: typeof PlaygroundForm; @@ -39,6 +44,7 @@ export const AdvancedPlayground: VFC<{ const theme = useTheme(); const matches = true; + const [configurationError, setConfigurationError] = useState(); const [environments, setEnvironments] = useState( value.environments ); @@ -137,12 +143,24 @@ export const AdvancedPlayground: VFC<{ if (action && typeof action === 'function') { action(); } + setConfigurationError(undefined); setResults(response); } catch (error: unknown) { - setToastData({ - type: 'error', - title: `Error parsing context: ${formatUnknownError(error)}`, - }); + if (error instanceof BadRequestError) { + setConfigurationError(error.message); + } else if (error instanceof SyntaxError) { + setToastData({ + type: 'error', + title: `Error parsing context: ${formatUnknownError( + error + )}`, + }); + } else { + setToastData({ + type: 'error', + title: formatUnknownError(error), + }); + } } }; @@ -242,6 +260,14 @@ export const AdvancedPlayground: VFC<{ padding: theme.spacing(4, 2), })} > + + {configurationError} + + } + /> } diff --git a/src/lib/features/playground/playground-service.ts b/src/lib/features/playground/playground-service.ts index 18fabe8cf9..8af5d726ee 100644 --- a/src/lib/features/playground/playground-service.ts +++ b/src/lib/features/playground/playground-service.ts @@ -15,6 +15,7 @@ import groupBy from 'lodash.groupby'; import { omitKeys } from '../../util'; import { AdvancedPlaygroundFeatureSchema } from '../../openapi/spec/advanced-playground-feature-schema'; import { AdvancedPlaygroundEnvironmentFeatureSchema } from '../../openapi/spec/advanced-playground-environment-feature-schema'; +import { validateQueryComplexity } from './validateQueryComplexity'; type EvaluationInput = { features: FeatureConfigurationClient[]; @@ -54,6 +55,12 @@ export class PlaygroundService { ); const contexts = generateObjectCombinations(context); + validateQueryComplexity( + environments.length, + environmentFeatures[0]?.features.length ?? 0, + contexts.length, + ); + const results = await Promise.all( environmentFeatures.flatMap( ({ features, featureProject, environment }) => diff --git a/src/lib/features/playground/validateQueryComplexity.test.ts b/src/lib/features/playground/validateQueryComplexity.test.ts new file mode 100644 index 0000000000..ba535722fa --- /dev/null +++ b/src/lib/features/playground/validateQueryComplexity.test.ts @@ -0,0 +1,39 @@ +import { validateQueryComplexity } from './validateQueryComplexity'; +import { BadDataError } from '../../error'; + +test('should not throw error when total combinations are under MAX_COMPLEXITY', () => { + const environmentsCount = 10; + const featuresCount = 10; + const contextCombinationsCount = 10; + + expect(() => + validateQueryComplexity( + environmentsCount, + featuresCount, + contextCombinationsCount, + ), + ).not.toThrow(); +}); + +test('should throw BadDataError when total combinations are over MAX_COMPLEXITY', () => { + const environmentsCount = 2; + const featuresCount = 200; + const contextCombinationsCount = 10000; + + const expectedMessage = `Rejecting evaluation as it would generate 4000000 combinations exceeding 30000 limit. Please reduce the number of selected environments (2), features (200), context field combinations (10000).`; + + expect(() => + validateQueryComplexity( + environmentsCount, + featuresCount, + contextCombinationsCount, + ), + ).toThrow(BadDataError); + expect(() => + validateQueryComplexity( + environmentsCount, + featuresCount, + contextCombinationsCount, + ), + ).toThrow(expectedMessage); +}); diff --git a/src/lib/features/playground/validateQueryComplexity.ts b/src/lib/features/playground/validateQueryComplexity.ts new file mode 100644 index 0000000000..2106455c46 --- /dev/null +++ b/src/lib/features/playground/validateQueryComplexity.ts @@ -0,0 +1,19 @@ +import { BadDataError } from '../../error'; + +const MAX_COMPLEXITY = 30000; + +export const validateQueryComplexity = ( + environmentsCount: number, + featuresCount: number, + contextCombinationsCount: number, +): void => { + const totalCount = + environmentsCount * featuresCount * contextCombinationsCount; + + const reason = `Rejecting evaluation as it would generate ${totalCount} combinations exceeding ${MAX_COMPLEXITY} limit. `; + const action = `Please reduce the number of selected environments (${environmentsCount}), features (${featuresCount}), context field combinations (${contextCombinationsCount}).`; + + if (totalCount > MAX_COMPLEXITY) { + throw new BadDataError(reason + action); + } +};