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

feat: [DHIS2-16688] replace rule engine #3955

Merged
merged 45 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
67fe038
build: add kotlin rule engine dependency
superskip Apr 18, 2024
8098458
feat: run new rule engine
superskip Jun 17, 2024
179a645
build: remove 4.0.3 resolution of react-scripts
superskip Jul 31, 2024
936e39b
feat: convert output effects and reorganize `rules`-folder
superskip Aug 13, 2024
b4c8240
fix: filter out duplicate program rule variables
superskip Aug 15, 2024
37cb3f7
fix: linter errors
superskip Aug 15, 2024
7f14e96
fix: display computed expressions in feedback/indicator widget
superskip Aug 19, 2024
a56b74d
fix: use correct enum value for `fieldType`
superskip Aug 20, 2024
a6170f1
fix: use input converter on enrollment dates
superskip Aug 27, 2024
6a4ac41
refactor: rename `rulesEngine.types.js` to `ruleEngine.types.js`
superskip Sep 3, 2024
4731dee
fix: assign effects
superskip Sep 4, 2024
4963402
fix: linter errors
superskip Sep 4, 2024
6f02dc5
fix: missing date in register new enrollment page
superskip Sep 24, 2024
b66e883
fix: format of converted event
superskip Sep 27, 2024
0607227
refactor: extract local date converter
superskip Sep 27, 2024
2be16a4
chore: jest-config rule-engine workaround
JoakimSM Oct 4, 2024
9ed93e9
refactor: upgrade rule-engine dependency
superskip Oct 11, 2024
6ca5ad7
refactor: adapt code to breaking change in kotlin rule engine
superskip Oct 17, 2024
bae27a8
fix: conversion of new events
superskip Nov 5, 2024
4adaf8c
feat: logging
superskip Nov 5, 2024
98c0cb3
test: add orgunit groups
superskip Dec 2, 2024
d1ac534
fix: general refinements
superskip Feb 4, 2025
e1e7cb1
test: fix unit tests
superskip Feb 28, 2025
f58f6de
Merge branch 'master' into DHIS2-16688
superskip Feb 28, 2025
236ea08
refactor: fix code smells
superskip Mar 4, 2025
cf49131
Merge branch 'master' into DHIS2-16688
superskip Mar 4, 2025
707eef2
fix: unit tests
superskip Mar 4, 2025
4504fe2
feat: switch between rule engine versions
superskip Mar 5, 2025
e1671e3
Merge branch 'master' into DHIS2-16688
superskip Mar 5, 2025
866c8ee
feat: log to console which rule engine is being used
superskip Mar 5, 2025
1fca362
fix: update unit tests
superskip Mar 5, 2025
7ae9d55
fix: filter out null-valued TEA values
superskip Mar 5, 2025
a4b0f95
fix: use logging function that works in production mode
superskip Mar 6, 2025
ffbe5a6
fix: [DHIS2-19161] use correct value types for program indicator vari…
superskip Mar 7, 2025
4bf6d26
fix: [DHIS2-19162] support option set valued DEs and TEAs
superskip Mar 10, 2025
d163af8
fix: code smells and unit tests
superskip Mar 10, 2025
5392a5a
fix: add null-check in case data element or attribute is not added to…
superskip Mar 17, 2025
3f7626a
Merge branch 'master' into DHIS2-16688
superskip Mar 17, 2025
d7740d8
fix: update kotlin rule engine library version
superskip Mar 18, 2025
1974f2d
fix: pass `programId` to `addRulesAndVariablesFromProgramIndicators`
superskip Mar 18, 2025
b284a66
fix: provide all program stage data elements to rule engine
superskip Mar 17, 2025
b0ac96e
fix: review comments
superskip Mar 18, 2025
f381f30
test: fix unit tests
superskip Mar 18, 2025
9c21424
fix: linter error
superskip Mar 18, 2025
5b3ad88
Merge branch 'master' into DHIS2-16688
superskip Mar 18, 2025
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
7 changes: 7 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
const config = {
moduleDirectories: ['core_modules', 'node_modules'],
transform: {
'^.+\\.m?[t|j]sx?$': require.resolve('./node_modules/@dhis2/cli-app-scripts/config/jest.transform.js'),
},
transformIgnorePatterns: ['/node_modules/(?!@dhis2/rule-engine/)'],
moduleNameMapper: {
'@dhis2/rule-engine(.*)': '<rootDir>/node_modules/@dhis2/rule-engine/rule-engine.mjs',
},
};

module.exports = config;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@dhis2/d2-ui-org-unit-tree": "^7.3.3",
"@dhis2/d2-ui-rich-text": "^7.4.0",
"@dhis2/d2-ui-sharing-dialog": "^7.3.3",
"@dhis2/rule-engine": "^3.3.9",
"@dhis2/ui": "^9.10.1",
"@dhis2-ui/calendar": "^10.4.0",
"@joakim_sm/react-infinite-calendar": "^2.4.2",
Expand All @@ -31,6 +32,7 @@
"d2-manifest": "^1.0.0",
"d2-utilizr": "^0.2.15",
"date-fns": "^1.29.0",
"eslint-import-resolver-exports": "^1.0.0-beta.5",
"history": "^5.3.0",
"leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4",
Expand Down
4 changes: 2 additions & 2 deletions packages/rules-engine/src/rulesEngine.types.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export type DataElement = {
name: string,
};

export type DataElements = { [elementId: string]: DataElement };
export type DataElements = { [elementId: ?string]: DataElement };

export type RuleVariable = {
variableValue: any,
Expand All @@ -132,7 +132,7 @@ export type TrackedEntityAttribute = {
};

export type TrackedEntityAttributes = {
[id: string]: TrackedEntityAttribute
[id: ?string]: TrackedEntityAttribute
};

export type OrgUnitGroup = $ReadOnly<{|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export type Enrollment = {
+enrolledAt?: string,
+occurredAt?: string,
+enrollmentId?: string,
+programName?: string,
+enrollmentStatus?: string,
};

export type Option = {
Expand Down
15 changes: 12 additions & 3 deletions src/components/AppLoader/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { loadMetaData, cacheSystemSettings } from 'capture-core/metaDataStoreLoa
import { buildMetaDataAsync, buildSystemSettingsAsync } from 'capture-core/metaDataMemoryStoreBuilders';
import { initControllersAsync } from 'capture-core/storageControllers';
import { DisplayException } from 'capture-core/utils/exceptions';
import { rulesEngine } from '../../core_modules/capture-core/rules/rulesEngine';
import { initRulesEngine } from '../../core_modules/capture-core/rules/rulesEngine';

function setLogLevel() {
const levels = {
Expand Down Expand Up @@ -157,15 +157,24 @@ export async function initializeAsync(
},
});

rulesEngine.setSelectedUserRoles(userRoles.map(({ id }) => id));

const systemSettings = await onQueryApi({
resource: 'system/info',
params: {
fields: 'dateFormat,serverTimeZoneId,calendar',
},
});

// initialize rule engine
let ruleEngineSettings;
try {
ruleEngineSettings = await onQueryApi({
resource: 'dataStore/capture/ruleEngine',
});
} catch {
ruleEngineSettings = { version: 'default' };
}
initRulesEngine(ruleEngineSettings.version, userRoles);

// initialize storage controllers
try {
await initControllersAsync(onCacheExpired, currentUserId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const FEATURES = Object.freeze({
newOrgUnitModeQueryParam: 'newOrgUnitModeQueryParam',
moreGenericErrorMessages: 'moreGenericErrorMessages',
sendEmptyScheduledAt: 'sendEmptyScheduledAt',
kotlinRuleEngine: 'kotlinRuleEngine',
});

// The first minor version that supports the feature
Expand All @@ -38,6 +39,7 @@ const MINOR_VERSION_SUPPORT = Object.freeze({
[FEATURES.newOrgUnitModeQueryParam]: 41,
[FEATURES.moreGenericErrorMessages]: 42,
[FEATURES.sendEmptyScheduledAt]: 41,
[FEATURES.kotlinRuleEngine]: 42,
});

export const hasAPISupportForFeature = (minorVersion: string | number, featureName: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
Enrollment,
TEIValues,
OrgUnit,
} from '@dhis2/rules-engine-javascript';
} from '../../../../rules/RuleEngine/types/ruleEngine.types';
import {
getApplicableRuleEffectsForTrackerProgram,
updateRulesEffects,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useEffect, useMemo, useState } from 'react';
import { convertValue } from '../../../../../converters/serverToClient';
import { getApplicableRuleEffectsForTrackerProgram } from '../../../../../rules';
import { dataElementTypes, type TrackerProgram } from '../../../../../metaData';
import { dataElementTypes, getTrackerProgramThrowIfNotFound, type TrackerProgram } from '../../../../../metaData';
import type { UseRuleEffectsInput } from './useRuleEffects.types';

// $FlowFixMe
Expand Down Expand Up @@ -48,12 +48,14 @@ const useEnrollmentData = enrollment => useMemo(() => {
return undefined;
}

const { enrollment: enrollmentId, enrolledAt, occurredAt } = enrollment;
const { enrollment: enrollmentId, enrolledAt, occurredAt, status, program } = enrollment;

return {
enrolledAt: convertDate(enrolledAt),
occurredAt: occurredAt ? convertDate(occurredAt) : undefined,
enrollmentId,
enrollmentStatus: status,
programName: getTrackerProgramThrowIfNotFound(program).name,
};
}, [enrollment]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
batchActionTypes as editEventDataEntryBatchActionTypes,
actionTypes as editEventDataEntryActionTypes,
} from '../editEventDataEntry.actions';
import { getProgramThrowIfNotFound } from '../../../../metaData';
import { getProgramThrowIfNotFound, dataElementTypes } from '../../../../metaData';
import { convertValue } from '../../../../converters/serverToClient';
import {
getCurrentClientValues,
getCurrentClientMainData,
Expand Down Expand Up @@ -85,7 +86,7 @@ const runRulesForEditSingleEvent = async ({
program,
stage,
orgUnit: coreOrgUnit,
currentEvent: { ...currentEvent, createdAt: apiCurrentEventOriginal.createdAt },
currentEvent: { ...currentEvent, createdAt: convertValue(apiCurrentEventOriginal.createdAt, dataElementTypes.DATETIME) },
otherEvents: prepareEnrollmentEventsForRulesEngine(apiOtherEvents),
enrollmentData: getEnrollmentForRulesEngine(enrollment),
attributeValues: getAttributeValuesForRulesEngine(attributeValues, program.attributes),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
// @flow
import { convertServerToClient } from '../../../converters';
import { dataElementTypes } from '../../../metaData';
import { dataElementTypes, getTrackerProgramThrowIfNotFound } from '../../../metaData';
import type {
EnrollmentData,
} from '../../Pages/common/EnrollmentOverviewDomain/useCommonEnrollmentDomainData';

export const getEnrollmentForRulesEngine = ({ enrolledAt, occurredAt, enrollment }: EnrollmentData = {}): { enrollmentId: string, enrolledAt: string, occurredAt?: string } => ({
export const getEnrollmentForRulesEngine = ({
enrolledAt,
occurredAt,
enrollment,
status,
program,
}: EnrollmentData = {}): {
enrollmentId: string,
enrolledAt: string,
occurredAt?: string,
enrollmentStatus: string,
programName: string,
} => ({
enrollmentId: enrollment,
// $FlowFixMe
enrolledAt: convertServerToClient(enrolledAt, dataElementTypes.DATE),
// $FlowFixMe
occurredAt: convertServerToClient(occurredAt, dataElementTypes.DATE),
enrollmentStatus: status,
programName: getTrackerProgramThrowIfNotFound(program).name,
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,31 @@ import type {
ProgramRulesContainer,
EventsData,
DataElements,
} from '@dhis2/rules-engine-javascript';
import { rulesEngine } from '../../../../rules/rulesEngine';
import type { RenderFoundation } from '../../../../metaData';
} from '../../../../rules/RuleEngine';
import { ruleEngine } from '../../../../rules/rulesEngine';
import {
dataElementTypes,
type RenderFoundation,
} from '../../../../metaData';
import {
updateRulesEffects,
postProcessRulesEffects,
buildEffectsHierarchy,
validateAssignEffects,
} from '../../../../rules';
import { convertServerToClient } from '../../../../converters';
import type { QuerySingleResource } from '../../../../utils/api';
import type { EnrollmentData } from '../Types';

const getEnrollmentForRulesExecution = enrollment =>
const getEnrollmentForRulesExecution = (enrollment: ?EnrollmentData, programName: string): ?Enrollment =>
enrollment && {
// $FlowFixMe[prop-missing]
enrollmentId: enrollment.enrollment,
enrolledAt: enrollment.enrolledAt,
occurredAt: enrollment.occurredAt,
// $FlowFixMe
enrolledAt: convertServerToClient(enrollment.enrolledAt, dataElementTypes.DATE),
// $FlowFixMe
occurredAt: convertServerToClient(enrollment.occurredAt, dataElementTypes.DATE),
enrollmentStatus: enrollment.status,
programName,
};

const getDataElementsForRulesExecution = (dataElements: ?DataElements) =>
Expand Down Expand Up @@ -54,26 +62,28 @@ export const getRulesActionsForTEI = ({
otherEvents,
dataElements,
userRoles,
programName,
}: {
foundation: RenderFoundation,
formId: string,
orgUnit: OrgUnit,
enrollmentData?: ?Enrollment,
enrollmentData?: EnrollmentData,
teiValues?: ?TEIValues,
trackedEntityAttributes: ?TrackedEntityAttributes,
optionSets: OptionSets,
rulesContainer: ProgramRulesContainer,
otherEvents?: ?EventsData,
dataElements: ?DataElements,
userRoles: Array<string>,
programName: string,
}) => {
const effects: OutputEffects = rulesEngine.getProgramRuleEffects({
const effects: OutputEffects = ruleEngine().getProgramRuleEffects({
programRulesContainer: rulesContainer,
currentEvent: null,
otherEvents,
dataElements: getDataElementsForRulesExecution(dataElements),
trackedEntityAttributes,
selectedEnrollment: getEnrollmentForRulesExecution(enrollmentData),
selectedEnrollment: getEnrollmentForRulesExecution(enrollmentData, programName),
selectedEntity: teiValues,
selectedOrgUnit: orgUnit,
selectedUserRoles: userRoles,
Expand All @@ -95,30 +105,33 @@ export const getRulesActionsForTEIAsync = async ({
otherEvents,
dataElements,
userRoles,
programName,
querySingleResource,
onGetValidationContext,
}: {
foundation: RenderFoundation,
formId: string,
orgUnit: OrgUnit,
enrollmentData?: ?Enrollment,
enrollmentData?: EnrollmentData,
teiValues?: ?TEIValues,
trackedEntityAttributes: ?TrackedEntityAttributes,
optionSets: OptionSets,
rulesContainer: ProgramRulesContainer,
otherEvents?: ?EventsData,
dataElements: ?DataElements,
userRoles: Array<string>,
programName: string,
querySingleResource: QuerySingleResource,
onGetValidationContext: () => Object,
}) => {
const effects: OutputEffects = rulesEngine.getProgramRuleEffects({
const effects: OutputEffects = ruleEngine().getProgramRuleEffects({
programRulesContainer: rulesContainer,
currentEvent: null,
otherEvents,
dataElements: getDataElementsForRulesExecution(dataElements),
trackedEntityAttributes,
selectedEnrollment: getEnrollmentForRulesExecution(enrollmentData),
// $FlowFixMe (flow doesn't understand that selectedEnrollment.enrolledAt/occurredAt are strings)
selectedEnrollment: getEnrollmentForRulesExecution(enrollmentData, programName),
selectedEntity: teiValues,
selectedOrgUnit: orgUnit,
selectedUserRoles: userRoles,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const addProgramRules = (program, programRules) => {
}));
};

const addRulesAndVariablesFromProgramIndicators = (program, programIndicators) => {
const addRulesAndVariablesFromProgramIndicators = (rulesContainer, programIndicators, programId) => {
const validProgramIndicators = programIndicators.filter((indicator) => {
if (!indicator.expression) {
log.error(
Expand All @@ -43,13 +43,13 @@ const addRulesAndVariablesFromProgramIndicators = (program, programIndicators) =
...programIndicator,
programId: getProgramId(programIndicator),
}));
const { rules, variables } = getRulesAndVariablesFromProgramIndicators(indicators, program.id);
const { rules, variables } = getRulesAndVariablesFromProgramIndicators(indicators, programId);

if (variables) {
program.programRuleVariables = [...program.programRuleVariables, ...variables];
rulesContainer.programRuleVariables = [...rulesContainer.programRuleVariables, ...variables];
}
if (rules) {
program.programRules = [...program.programRules, ...rules];
rulesContainer.programRules = [...rulesContainer.programRules, ...rules];
}
};

Expand All @@ -70,7 +70,7 @@ export const buildRulesContainer = async ({

programRuleVariables && addProgramVariables(rulesContainer, programRuleVariables);
programRules && addProgramRules(rulesContainer, programRules);
programIndicators && addRulesAndVariablesFromProgramIndicators(rulesContainer, programIndicators);
programIndicators && addRulesAndVariablesFromProgramIndicators(rulesContainer, programIndicators, programAPI.id);
rulesContainer.constants = constants;

setRulesContainer(rulesContainer);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// @flow
export type EnrollmentData = {
enrollment: string,
enrolledAt: string,
occurredAt: string,
status: string,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @flow
export { EnrollmentData } from './EnrollmentData.type';
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// @flow
import { batchActions } from 'redux-batched-actions';
import { convertGeometryOut } from 'capture-core/components/DataEntries/converters';
import type {
OrgUnit,
TrackedEntityAttributes,
OptionSets,
ProgramRulesContainer,
EventsData,
DataElements,
Enrollment,
} from '@dhis2/rules-engine-javascript';
import { convertGeometryOut } from 'capture-core/components/DataEntries/converters';
} from '../../../rules/RuleEngine';
import type { EnrollmentData } from './Types';
import { actionCreator } from '../../../actions/actions.utils';
import { effectMethods } from '../../../trackerOffline';
import type { RenderFoundation } from '../../../metaData';
Expand Down Expand Up @@ -54,13 +54,14 @@ const dataEntryPropsToInclude: Array<Object> = [

type Context = {
orgUnit: OrgUnit,
programName: string,
trackedEntityAttributes: ?TrackedEntityAttributes,
optionSets: OptionSets,
rulesContainer: ProgramRulesContainer,
formFoundation: RenderFoundation,
otherEvents?: ?EventsData,
dataElements: ?DataElements,
enrollment?: ?Enrollment,
enrollment?: EnrollmentData,
userRoles: Array<string>,
state: ReduxState,
};
Expand Down Expand Up @@ -89,6 +90,7 @@ export const getUpdateFieldActions = async ({
dataElements,
enrollment,
userRoles,
programName,
} = context;
const { dataEntryId, itemId, elementId, value, uiState } = innerAction.payload || {};
const fieldData: FieldData = {
Expand All @@ -110,6 +112,7 @@ export const getUpdateFieldActions = async ({
otherEvents,
dataElements,
userRoles,
programName,
querySingleResource,
onGetValidationContext,
});
Expand Down
Loading
Loading