Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [DHIS2-17854] validate the assigned values from rules engine #3783

Merged
merged 31 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3d237e0
fix: validate the assigned values from rules engine
simonadomnisoru Aug 29, 2024
c367c1f
Merge branch 'master' into DHIS2-17854
simonadomnisoru Aug 29, 2024
3e8a59d
fix: validate the assigned values from rules engine
simonadomnisoru Sep 2, 2024
dd8f672
fix: validate the assigned values from rules engine
simonadomnisoru Sep 3, 2024
5df8667
chore: small improvements
simonadomnisoru Sep 3, 2024
71b5fa6
feat: extract validateField from FormBuilder into common external fun…
simonadomnisoru Sep 4, 2024
e3f6894
chore: improve ValidatorContainer type
simonadomnisoru Sep 9, 2024
ffa3ed8
chore: replace EMPTY with of()
simonadomnisoru Oct 3, 2024
5091965
chore: rollback formSectionFields checks
simonadomnisoru Oct 3, 2024
e855660
chore: replace assigned effects with a one-element array & lastIndex …
simonadomnisoru Oct 3, 2024
067bb56
Merge branch 'master' into DHIS2-17854
simonadomnisoru Oct 3, 2024
fcf1b3b
chore: variable rename
simonadomnisoru Oct 3, 2024
4d01a72
chore: rename validateField to validateValue
simonadomnisoru Oct 9, 2024
98dea41
chore: move the files to capture-core/rules and capture-core/utils/va…
simonadomnisoru Oct 10, 2024
0c6a995
chore: rename callback
simonadomnisoru Oct 10, 2024
f18f2c7
chore: remove object destructering
simonadomnisoru Oct 10, 2024
87d0d78
fix: properly update dataEntriesInProgressList while async validation…
simonadomnisoru Oct 23, 2024
64db572
fix: disable save button in WidgetProfile while async validation is r…
simonadomnisoru Oct 23, 2024
245bee6
Merge branch 'master' into DHIS2-17854
simonadomnisoru Oct 23, 2024
44990c1
Merge branch 'master' into DHIS2-17854
simonadomnisoru Nov 4, 2024
3b0a8fb
Merge branch 'master' into DHIS2-17854
simonadomnisoru Nov 6, 2024
54d42c7
Merge branch 'master' into DHIS2-17854
simonadomnisoru Nov 13, 2024
86bbb6e
chore: skip assigned values validation when opening forms
simonadomnisoru Nov 21, 2024
2ecf300
chore: skip assigned values validation when opening forms
simonadomnisoru Nov 21, 2024
8dfd816
Merge branch 'master' into DHIS2-17854
simonadomnisoru Nov 21, 2024
360933e
chore: skip assigned values validation when opening new event forms
simonadomnisoru Nov 25, 2024
7cf7867
Merge branch 'master' into DHIS2-17915
simonadomnisoru Nov 25, 2024
ed637cb
Merge branch 'master' into DHIS2-17854
simonadomnisoru Dec 3, 2024
fefbdc6
fix: flow errors
simonadomnisoru Dec 3, 2024
a19ed6e
fix: flow errors
simonadomnisoru Dec 3, 2024
d7ee45c
fix: stabilize flacky cypress tests
simonadomnisoru Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,18 @@ import isObject from 'd2-utilizr/lib/isObject';
import defaultClasses from './formBuilder.module.css';
import type { ErrorData, PostProcessErrorMessage } from './formbuilder.types';
import type { PluginContext } from '../FormFieldPlugin/FormFieldPlugin.types';
import { getValidators } from '../field/validators';
import { getValidators, validateField } from '../field/validators';
import type { ValidatorContainer } from '../field/validators';
import type { DataElement } from '../../../metaData';
import type { QuerySingleResource } from '../../../utils/api';

export type ValidatorContainer = {
validator: (value: any, validationContext: ?Object) => boolean | Promise<boolean>,
message: string,
validatingMessage?: ?string,
type?: ?string,
async?: ?boolean,
};

export type FieldConfig = {
id: string,
component: React.ComponentType<any>,
plugin?: boolean,
props: Object,
validators?: ?Array<ValidatorContainer>,
validators?: Array<ValidatorContainer>,
commitEvent?: ?string,
onIsEqual?: ?(newValue: any, oldValue: any) => boolean,
};
Expand All @@ -47,7 +41,7 @@ type GetContainerPropsFn = (index: number, fieldsCount: number, field: FieldConf

type FieldCommitConfig = {|
fieldId: string,
validators?: ?Array<ValidatorContainer>,
validators?: Array<ValidatorContainer>,
onIsEqual?: ?(newValue: any, oldValue: any) => boolean,
|}

Expand Down Expand Up @@ -99,54 +93,6 @@ type FieldCommitOptions = {
type FieldsValidatingPromiseContainer = { [fieldId: string]: ?{ cancelableValidatingPromise?: ?CancelablePromise<any>, validatingCompleteUid: string } };

export class FormBuilder extends React.Component<Props> {
static async validateField(
{ validators }: { validators?: ?Array<ValidatorContainer> },
value: any,
validationContext: ?Object,
onIsValidatingInternal: ?Function,
): Promise<{ valid: boolean, errorMessage?: ?string, errorType?: ?string }> {
if (!validators || validators.length === 0) {
return {
valid: true,
};
}

const validatorResult = await validators
.reduce(async (passPromise, currentValidator) => {
const pass = await passPromise;
if (pass === true) {
let result = currentValidator.validator(value, validationContext);
if (result instanceof Promise) {
result = onIsValidatingInternal ? onIsValidatingInternal(currentValidator.validatingMessage, result) : result;
result = await result;
}

if (result === true || (result && result.valid)) {
return true;
}
return {
message: (result && result.errorMessage) || currentValidator.message,
type: currentValidator.type,
data: result && result.data,
};
}
return pass;
}, Promise.resolve(true));

if (validatorResult !== true) {
return {
valid: false,
errorMessage: validatorResult.message,
errorType: validatorResult.type,
errorData: validatorResult.data,
};
}

return {
valid: true,
};
}

static getAsyncUIState(fieldsUI: { [id: string]: FieldUI }) {
return Object.keys(fieldsUI).reduce((accAsyncUIState, fieldId) => {
const fieldUI = fieldsUI[fieldId];
Expand Down Expand Up @@ -219,7 +165,7 @@ export class FormBuilder extends React.Component<Props> {

let validationData;
try {
validationData = await FormBuilder.validateField(
validationData = await validateField(
field,
values[field.id],
validationContext,
Expand Down Expand Up @@ -429,7 +375,7 @@ export class FormBuilder extends React.Component<Props> {
};

this.commitUpdateTriggeredForFields[fieldId] = true;
const updatePromise = FormBuilder.validateField(
const updatePromise = validateField(
{ validators },
value,
onGetValidationContext && onGetValidationContext(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @flow
export { FormBuilder } from './FormBuilder.component';
export type { PostProcessErrorMessage, ErrorData } from './formbuilder.types';
export type { FieldConfig, ValidatorContainer } from './FormBuilder.component';
export type { FieldConfig } from './FormBuilder.component';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import { type ComponentType } from 'react';
import type { ValidatorContainer } from '../../../FormBuilder';
import type { ValidatorContainer } from '../../../field/validators';
import { getValidators } from '../../validators';
import type { DataElement } from '../../../../../metaData';
import type { QuerySingleResource } from '../../../../../utils/api/api.types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import { dataElementTypes, type DateDataElement, type DataElement } from '../../
import { validatorTypes } from './constants';
import type { QuerySingleResource } from '../../../../utils/api/api.types';

type Validator = (value: any) => Promise<boolean> | boolean | { valid: boolean, errorMessage?: any};
type Validator = (
value: any,
contextProps: ?Object,
) => Promise<boolean> | boolean | { valid: boolean, errorMessage?: any, data?: any };

export type ValidatorContainer = {
validator: Validator,
Expand Down Expand Up @@ -205,7 +208,7 @@ const validatorsForTypes = {
}],
};

function buildTypeValidators(metaData: DataElement | DateDataElement): ?Array<ValidatorContainer> {
function buildTypeValidators(metaData: DataElement | DateDataElement): Array<ValidatorContainer> {
// $FlowFixMe dataElementTypes flow error
let validatorContainersForType = validatorsForTypes[metaData.type] ? validatorsForTypes[metaData.type] : [];

Expand All @@ -226,7 +229,7 @@ function buildTypeValidators(metaData: DataElement | DateDataElement): ?Array<Va
return validatorContainersForType;
}

function buildCompulsoryValidator(metaData: DataElement): Array<?ValidatorContainer> {
function buildCompulsoryValidator(metaData: DataElement): Array<ValidatorContainer> {
return metaData.compulsory
?
[
Expand All @@ -243,7 +246,7 @@ function buildCompulsoryValidator(metaData: DataElement): Array<?ValidatorContai
function buildUniqueValidator(
metaData: DataElement,
querySingleResource: QuerySingleResource,
): Array<?ValidatorContainer> {
): Array<ValidatorContainer> {
return metaData.unique
?
[
Expand All @@ -265,7 +268,7 @@ function buildUniqueValidator(
}

export const getValidators =
(metaData: DataElement, querySingleResource: QuerySingleResource): Array<?ValidatorContainer> => [
(metaData: DataElement, querySingleResource: QuerySingleResource): Array<ValidatorContainer> => [
buildCompulsoryValidator,
buildTypeValidators,
buildUniqueValidator,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
// @flow
export { getValidators } from './getValidators';
export { validateField } from './validateField';
export { validateAssignEffects } from './validateAssignEffects';

export type { ValidatorContainer } from './getValidators';
export type { AssignOutputEffectWithValidations } from './validateAssignEffects';
JoakimSM marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// @flow
import { errorCreator } from 'capture-core-utils';
import { effectActions } from '@dhis2/rules-engine-javascript';
import log from 'loglevel';
import type { AssignOutputEffect } from '@dhis2/rules-engine-javascript';
import { type DataElement } from '../../../../metaData';
import type { QuerySingleResource } from '../../../../utils/api';
import { getValidators } from './getValidators';
import type { Validations } from './validateField';
import { validateField } from './validateField';

export type AssignOutputEffectWithValidations = {
[metaDataId: string]: Array<AssignOutputEffect & Validations>,
};

export const validateAssignEffects = async ({
dataElements,
effects,
querySingleResource,
onGetValidationContext,
}: {
dataElements: Array<DataElement>,
effects: Object,
querySingleResource: QuerySingleResource,
onGetValidationContext?: () => Object,
}): Promise<?AssignOutputEffectWithValidations> => {
const assignEffects: {| [metaDataId: string]: Array<AssignOutputEffect> |} = effects[effectActions.ASSIGN_VALUE];
if (!assignEffects) {
return effects;
}

const assignEffectsWithValidations = await dataElements.reduce(async (passPromise, metaData: DataElement) => {
const acc = await passPromise;
if (!assignEffects[metaData.id]) {
return acc;
}

const effectsForId = assignEffects[metaData.id];
const lastEffect = effectsForId.length - 1;
superskip marked this conversation as resolved.
Show resolved Hide resolved
const value = effectsForId[lastEffect].value;
const validators = getValidators(metaData, querySingleResource);
const validationContext = onGetValidationContext && onGetValidationContext();

try {
const validatorResult = await validateField({ validators }, value, validationContext);
const effectWithValidation = Object.assign({}, effectsForId[lastEffect], validatorResult);

acc[metaData.id] = [...effectsForId.slice(0, lastEffect - 1), effectWithValidation];
superskip marked this conversation as resolved.
Show resolved Hide resolved
return acc;
} catch (error) {
log.error(
errorCreator('an error occured while validating the assigned program rule effect')({
metaData,
lastEffect,
error,
}),
);
return acc;
}
}, Promise.resolve({}));

return { ...effects, [effectActions.ASSIGN_VALUE]: assignEffectsWithValidations };
};
JoakimSM marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// @flow
import type { ValidatorContainer } from './getValidators';

export type Validations = {
valid: boolean,
errorMessage?: ?string,
errorType?: ?string,
errorData?: Object,
};

export const validateField = async (
{ validators }: { validators?: Array<ValidatorContainer> },
JoakimSM marked this conversation as resolved.
Show resolved Hide resolved
value: any,
validationContext: ?Object,
onIsValidatingInternal: ?Function,
JoakimSM marked this conversation as resolved.
Show resolved Hide resolved
): Promise<Validations> => {
if (!validators || validators.length === 0) {
return {
valid: true,
};
}

const validatorResult = await validators.reduce(async (passPromise, currentValidator) => {
const pass = await passPromise;
if (pass === true) {
let result = currentValidator.validator(value, validationContext);
if (result instanceof Promise) {
result = onIsValidatingInternal
? onIsValidatingInternal(currentValidator.validatingMessage, result)
: result;
result = await result;
}

if (result === true || (result && result.valid)) {
return true;
}
return {
message: (result && result.errorMessage) || currentValidator.message,
type: currentValidator.type,
data: result && result.data,
};
}
return pass;
}, Promise.resolve(true));

if (validatorResult !== true) {
return {
valid: false,
errorMessage: validatorResult.message,
errorType: validatorResult.type,
errorData: validatorResult.data,
};
}

return {
valid: true,
};
};
2 changes: 2 additions & 0 deletions src/core_modules/capture-core/components/D2Form/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @flow
export { asyncHandlerActionTypes, asyncUpdateFieldEpic } from './asyncHandlerHOC';
export { D2Form } from './D2Form.container';
export { validateAssignEffects } from './field/validators';
export type { AssignOutputEffectWithValidations } from './field/validators';
Original file line number Diff line number Diff line change
Expand Up @@ -419,17 +419,38 @@ export class EnrollmentDataEntryComponent extends React.Component<PreEnrollmentD

handleUpdateField = (...args: Array<any>) => {
const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props;
this.props.onUpdateField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation);
}
this.props.onUpdateField(
...args,
programId,
orgUnit,
firstStageMetaData?.stage,
formFoundation,
this.getValidationContext,
);
};

handleUpdateDataEntryField = (...args: Array<any>) => {
const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props;
this.props.onUpdateDataEntryField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation);
this.props.onUpdateDataEntryField(
...args,
programId,
orgUnit,
firstStageMetaData?.stage,
formFoundation,
this.getValidationContext,
);
}

handleStartAsyncUpdateField = (...args: Array<any>) => {
const { programId, orgUnit, firstStageMetaData, formFoundation } = this.props;
this.props.onStartAsyncUpdateField(...args, programId, orgUnit, firstStageMetaData?.stage, formFoundation);
this.props.onStartAsyncUpdateField(
...args,
programId,
orgUnit,
firstStageMetaData?.stage,
formFoundation,
this.getValidationContext,
);
}

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({
orgUnit: OrgUnit,
stage?: ProgramStage,
formFoundation: RenderFoundation,
onGetValidationContext: () => Object,
) => {
dispatch(updateDataEntryFieldBatch(innerAction, programId, orgUnit, stage, formFoundation));
dispatch(
updateDataEntryFieldBatch(innerAction, programId, orgUnit, stage, formFoundation, onGetValidationContext),
);
},
onUpdateField: (
innerAction: ReduxAction<any, any>,
programId: string,
orgUnit: OrgUnit,
stage?: ProgramStage,
formFoundation: RenderFoundation,
onGetValidationContext: () => Object,
) => {
dispatch(updateFieldBatch(innerAction, programId, orgUnit, stage, formFoundation));
dispatch(updateFieldBatch(innerAction, programId, orgUnit, stage, formFoundation, onGetValidationContext));
},
onStartAsyncUpdateField: (
innerAction: ReduxAction<any, any>,
Expand All @@ -34,9 +38,10 @@ const mapDispatchToProps = (dispatch: ReduxDispatch) => ({
orgUnit: OrgUnit,
stage?: ProgramStage,
formFoundation: RenderFoundation,
onGetValidationContext: () => Object,
) => {
const onAsyncUpdateSuccess = (successInnerAction: ReduxAction<any, any>) =>
asyncUpdateSuccessBatch(successInnerAction, dataEntryId, itemId, programId, orgUnit, stage, formFoundation);
asyncUpdateSuccessBatch(successInnerAction, dataEntryId, itemId, programId, orgUnit, stage, formFoundation, onGetValidationContext);
const onAsyncUpdateError = (errorInnerAction: ReduxAction<any, any>) => errorInnerAction;

dispatch(startAsyncUpdateFieldForNewEnrollment(innerAction, onAsyncUpdateSuccess, onAsyncUpdateError));
Expand Down
Loading
Loading