Skip to content

Commit

Permalink
move rulesGeneration into separate module + align errors
Browse files Browse the repository at this point in the history
  • Loading branch information
paula-stacho committed Mar 7, 2025
1 parent 4d326f7 commit a48c7fa
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 162 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {
Banner,
BannerVariant,
Button,
ButtonVariant,
CancelLoader,
Expand All @@ -22,7 +23,9 @@ import {
clearRulesGenerationError,
generateValidationRules,
stopRulesGeneration,
} from '../modules/validation';
type RulesGenerationError,
} from '../modules/rules-generation';
import { DISTINCT_FIELDS_ABORT_THRESHOLD } from '@mongodb-js/compass-schema';

const validationStatesStyles = css({
padding: spacing[400],
Expand Down Expand Up @@ -67,7 +70,7 @@ const DOC_UPGRADE_REVISION =
type ValidationStatesProps = {
isZeroState: boolean;
isRulesGenerationInProgress?: boolean;
rulesGenerationError?: string;
rulesGenerationError?: RulesGenerationError;
isLoaded: boolean;
changeZeroState: (value: boolean) => void;
generateValidationRules: () => void;
Expand Down Expand Up @@ -144,6 +147,51 @@ const GeneratingScreen: React.FunctionComponent<{
);
};

const RulesGenerationErrorBanner: React.FunctionComponent<{
error: RulesGenerationError;
onDismissError: () => void;
}> = ({ error, onDismissError }) => {
if (error?.errorType === 'timeout') {
return (
<WarningSummary
data-testid="rules-generation-timeout-message"
warnings={[
'Operation exceeded time limit. Please try increasing the maxTimeMS for the query in the filter options.',
]}
dismissible={true}
onClose={onDismissError}
/>
);
}
if (error?.errorType === 'highComplexity') {
return (
<Banner
variant={BannerVariant.Danger}
data-testid="rules-generation-complexity-abort-message"
dismissible={true}
onClose={onDismissError}
>
The rules generation was aborted because the number of fields exceeds{' '}
{DISTINCT_FIELDS_ABORT_THRESHOLD}. Consider breaking up your data into
more collections with smaller documents, and using references to
consolidate the data you need.&nbsp;
<Link href="https://www.mongodb.com/docs/manual/data-modeling/design-antipatterns/bloated-documents/">
Learn more
</Link>
</Banner>
);
}

return (
<ErrorSummary
data-testid="rules-generation-error-message"
errors={[`Error occured during rules generation: ${error.errorMessage}`]}
dismissible={true}
onClose={onDismissError}
/>
);
};

export function ValidationStates({
isZeroState,
isRulesGenerationInProgress,
Expand Down Expand Up @@ -173,10 +221,9 @@ export function ValidationStates({
data-testid="schema-validation-states"
>
{rulesGenerationError && (
<ErrorSummary
errors={rulesGenerationError}
dismissible={true}
onClose={clearRulesGenerationError}
<RulesGenerationErrorBanner
error={rulesGenerationError}
onDismissError={clearRulesGenerationError}
/>
)}
<ValidationBanners editMode={editMode} />
Expand Down Expand Up @@ -248,8 +295,8 @@ const mapStateToProps = (state: RootState) => ({
isZeroState: state.isZeroState,
isLoaded: state.isLoaded,
editMode: state.editMode,
isRulesGenerationInProgress: state.validation.isRulesGenerationInProgress,
rulesGenerationError: state.validation.rulesGenerationError,
isRulesGenerationInProgress: state.rulesGeneration.isInProgress,
rulesGenerationError: state.rulesGeneration.error,
});

/**
Expand Down
10 changes: 9 additions & 1 deletion packages/compass-schema-validation/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ import type AppRegistry from 'hadron-app-registry';
import type { Logger } from '@mongodb-js/compass-logging/provider';
import type { TrackFunction } from '@mongodb-js/compass-telemetry';
import { type WorkspacesService } from '@mongodb-js/compass-workspaces/provider';
import type { RulesGenerationState } from './rules-generation';
import {
INITIAL_STATE as RULES_GENERATION_STATE,
rulesGenerationReducer,
} from './rules-generation';

/**
* Reset action constant.
Expand All @@ -44,6 +49,7 @@ export interface RootState {
namespace: NamespaceState;
serverVersion: ServerVersionState;
validation: ValidationState;
rulesGeneration: RulesGenerationState;
sampleDocuments: SampleDocumentState;
isZeroState: IsZeroStateState;
isLoaded: IsLoadedState;
Expand Down Expand Up @@ -92,6 +98,7 @@ export const INITIAL_STATE: RootState = {
namespace: NS_INITIAL_STATE,
serverVersion: SV_INITIAL_STATE,
validation: VALIDATION_STATE,
rulesGeneration: RULES_GENERATION_STATE,
sampleDocuments: SAMPLE_DOCUMENTS_STATE,
isZeroState: IS_ZERO_STATE,
isLoaded: IS_LOADED_STATE,
Expand All @@ -101,14 +108,15 @@ export const INITIAL_STATE: RootState = {
/**
* The reducer.
*/
const appReducer = combineReducers<RootState, RootAction>({
const appReducer = combineReducers<RootState, AnyAction>({
namespace,
serverVersion,
validation,
sampleDocuments,
isZeroState,
isLoaded,
editMode,
rulesGeneration: rulesGenerationReducer,
});

/**
Expand Down
223 changes: 223 additions & 0 deletions packages/compass-schema-validation/src/modules/rules-generation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import type { SchemaValidationThunkAction } from '.';
import { zeroStateChanged } from './zero-state';
import { enableEditRules } from './edit-mode';
import { analyzeSchema } from '@mongodb-js/compass-schema';
import type { MongoError } from 'mongodb';
import type { Action, AnyAction, Reducer } from 'redux';
import { validationLevelChanged, validatorChanged } from './validation';

export function isAction<A extends AnyAction>(
action: AnyAction,
type: A['type']
): action is A {
return action.type === type;
}

export type ValidationServerAction = 'error' | 'warn';
export type ValidationLevel = 'off' | 'moderate' | 'strict';

const ERROR_CODE_MAX_TIME_MS_EXPIRED = 50;

const SAMPLE_SIZE = 1000;
const ABORT_MESSAGE = 'Operation cancelled';

export const enum RulesGenerationActions {
generationStarted = 'schema-validation/rules-generation/generationStarted',
generationFailed = 'schema-validation/rules-generation/generationFailed',
generationFinished = 'schema-validation/rules-generation/generationFinished',
generationErrorCleared = 'schema-validation/rules-generation/generationErrorCleared',
}

export type RulesGenerationStarted = {
type: RulesGenerationActions.generationStarted;
};

export type RulesGenerationFailed = {
type: RulesGenerationActions.generationFailed;
error: Error;
};

export type RulesGenerationErrorCleared = {
type: RulesGenerationActions.generationErrorCleared;
};

export type RulesGenerationFinished = {
type: RulesGenerationActions.generationFinished;
};

export type RulesGenerationError = {
errorMessage: string;
errorType: 'timeout' | 'highComplexity' | 'general';
};

export interface RulesGenerationState {
isInProgress: boolean;
error?: RulesGenerationError;
}

/**
* The initial state.
*/
export const INITIAL_STATE: RulesGenerationState = {
isInProgress: false,
};

function getErrorDetails(error: Error): RulesGenerationError {
const errorCode = (error as MongoError).code;
const errorMessage = error.message || 'Unknown error';
let errorType: RulesGenerationError['errorType'] = 'general';
if (errorCode === ERROR_CODE_MAX_TIME_MS_EXPIRED) {
errorType = 'timeout';
} else if (error.message.includes('Schema analysis aborted: Fields count')) {
errorType = 'highComplexity';
}

return {
errorType,
errorMessage,
};
}

/**
* Reducer function for handle state changes to status.
*/
export const rulesGenerationReducer: Reducer<RulesGenerationState, Action> = (
state = INITIAL_STATE,
action
) => {
if (
isAction<RulesGenerationStarted>(
action,
RulesGenerationActions.generationStarted
)
) {
return {
...state,
isInProgress: true,
error: undefined,
};
}

if (
isAction<RulesGenerationFinished>(
action,
RulesGenerationActions.generationFinished
)
) {
return {
...state,
isInProgress: false,
};
}

if (
isAction<RulesGenerationFailed>(
action,
RulesGenerationActions.generationFailed
)
) {
return {
...state,
isInProgress: false,
error: getErrorDetails(action.error),
};
}

if (
isAction<RulesGenerationErrorCleared>(
action,
RulesGenerationActions.generationErrorCleared
)
) {
return {
...state,
error: undefined,
};
}

return state;
};

export const clearRulesGenerationError =
(): SchemaValidationThunkAction<RulesGenerationErrorCleared> => {
return (dispatch) =>
dispatch({ type: RulesGenerationActions.generationErrorCleared });
};

export const stopRulesGeneration = (): SchemaValidationThunkAction<void> => {
return (dispatch, getState, { rulesGenerationAbortControllerRef }) => {
if (!rulesGenerationAbortControllerRef.current) return;
rulesGenerationAbortControllerRef.current?.abort(ABORT_MESSAGE);
};
};

/**
* Get $jsonSchema from schema analysis
* @returns
*/
export const generateValidationRules = (): SchemaValidationThunkAction<
Promise<void>
> => {
return async (
dispatch,
getState,
{ dataService, logger, preferences, rulesGenerationAbortControllerRef }
) => {
dispatch({ type: RulesGenerationActions.generationStarted });

rulesGenerationAbortControllerRef.current = new AbortController();
const abortSignal = rulesGenerationAbortControllerRef.current.signal;

const { namespace } = getState();
const { maxTimeMS } = preferences.getPreferences();

try {
const samplingOptions = {
query: {},
size: SAMPLE_SIZE,
fields: undefined,
};
const driverOptions = {
maxTimeMS,
};
const schemaAccessor = await analyzeSchema(
dataService,
abortSignal,
namespace.toString(),
samplingOptions,
driverOptions,
logger,
preferences
);
if (abortSignal?.aborted) {
throw new Error(ABORT_MESSAGE);
}

const jsonSchema = await schemaAccessor?.getMongoDBJsonSchema({
signal: abortSignal,
});
if (abortSignal?.aborted) {
throw new Error(ABORT_MESSAGE);
}
const validator = JSON.stringify(
{ $jsonSchema: jsonSchema },
undefined,
2
);
dispatch(validationLevelChanged('moderate'));
dispatch(validatorChanged(validator));
dispatch(enableEditRules());
dispatch({ type: RulesGenerationActions.generationFinished });
dispatch(zeroStateChanged(false));
} catch (error) {
if (abortSignal.aborted) {
dispatch({ type: RulesGenerationActions.generationFinished });
return;
}
dispatch({
type: RulesGenerationActions.generationFailed,
error,
});
}
};
};
Loading

0 comments on commit a48c7fa

Please sign in to comment.