From 8b05fd95d78937e69d9b66da2d6cfbf3277b0521 Mon Sep 17 00:00:00 2001 From: vscaiceanu-1a <86055112+vscaiceanu-1a@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:09:13 +0200 Subject: [PATCH] fix: linked components should take validity range into account --- .../rules-engine.runner.service.spec.ts | 26 +++-- .../rulesets/rulesets.selectors.spec.ts | 106 ++++++++++++++++-- .../src/stores/rulesets/rulesets.selectors.ts | 33 +++--- .../onerulesetvalid-onerule-nocond.mock.ts | 9 +- 4 files changed, 139 insertions(+), 35 deletions(-) diff --git a/packages/@o3r/rules-engine/src/services/runner/rules-engine.runner.service.spec.ts b/packages/@o3r/rules-engine/src/services/runner/rules-engine.runner.service.spec.ts index 5fee401e6e..e336aa7108 100644 --- a/packages/@o3r/rules-engine/src/services/runner/rules-engine.runner.service.spec.ts +++ b/packages/@o3r/rules-engine/src/services/runner/rules-engine.runner.service.spec.ts @@ -5,7 +5,7 @@ import { EffectsModule } from '@ngrx/effects'; import { select, Store, StoreModule } from '@ngrx/store'; import { computeItemIdentifier } from '@o3r/core'; import { BehaviorSubject, firstValueFrom, Observable, of, Subject } from 'rxjs'; -import { distinctUntilChanged, map, take } from 'rxjs/operators'; +import { distinctUntilChanged, map } from 'rxjs/operators'; import { jsonOneRulesetOneRuleNoCondPlaceholder } from '../../../testing/mocks/oneruleset-onerule-nocond-placeholder.mock'; import { jsonOneRulesetOneRuleNoCond } from '../../../testing/mocks/oneruleset-onerule-nocond.mock'; import { jsonOneRulesetOneRuleReexecution } from '../../../testing/mocks/oneruleset-onerule-reexecution.mock'; @@ -100,15 +100,25 @@ describe('Rules engine service', () => { expect(actions[0].actionType).toBe('UPDATE_LOCALISATION'); }); - it('should handle linked components and validity range properly', (done) => { - service.events$.pipe(take(1)).subscribe((actions) => { - expect(actions.length).toBe(1); - expect(actions[0].actionType).toBe('UPDATE_LOCALISATION'); - done(); - }); + it('should handle linked components and validity range properly', async () => { store.dispatch(setRulesetsEntities({entities: jsonOneRulesetValidOneRuleNoCond.ruleSets})); - }); + const actions = await firstValueFrom(service.events$); + expect(actions.length).toBe(1); + expect(actions[0].actionType).toBe('UPDATE_LOCALISATION'); + expect(actions[0].value).toBe('my.custom.ssci.loc.key2'); + service.enableRuleSetFor(computeItemIdentifier('TestComponent', '@otter/comps')); + const nextActions = await firstValueFrom(service.events$); + expect(nextActions.length).toBe(2); + expect(nextActions[1].actionType).toBe('UPDATE_LOCALISATION'); + expect(nextActions[1].value).toBe('my.custom.ssci.loc.key3'); + // out of range validity for ruleset linked to TestComponent2 + service.enableRuleSetFor(computeItemIdentifier('TestComponent2', '@otter/comps')); + const lastActions = await firstValueFrom(service.events$); + expect(lastActions.length).toBe(2); + expect(lastActions[1].actionType).toBe('UPDATE_LOCALISATION'); + expect(lastActions[1].value).toBe('my.custom.ssci.loc.key3'); + }); it('should have configuration updated in the store', async () => { service.engine.upsertFacts([{ diff --git a/packages/@o3r/rules-engine/src/stores/rulesets/rulesets.selectors.spec.ts b/packages/@o3r/rules-engine/src/stores/rulesets/rulesets.selectors.spec.ts index 5daa8023dc..2f4c5a6b62 100644 --- a/packages/@o3r/rules-engine/src/stores/rulesets/rulesets.selectors.spec.ts +++ b/packages/@o3r/rules-engine/src/stores/rulesets/rulesets.selectors.spec.ts @@ -100,6 +100,31 @@ describe('RuleSets Selector tests', () => { } }; + const r9: RulesetsModel = { + id: 'r9', + name: 'r9_name', + rules: [], + validityRange: { + from: afterTomorrow.toISOString() + }, + linkedComponent: { + library: '@mylibrary', + name: 'thisComponentWillBeIgnored' + }, + linkedComponents: { + or: [ + { + library: '@mylibrary', + name: 'mycomponent' + }, + { + library: '@mylibrary', + name: 'mycomponent2' + } + ] + } + }; + it('should return the ruleset in range', () => { const state: RulesetsState = { ids: [r1.id, r2.id], @@ -110,8 +135,9 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); - expect(selectors.selectActiveRuleSets.projector(allRuleSetsArray)).toEqual(['r1']); + expect(selectors.selectActiveRuleSets.projector(rulesetsInRange)).toEqual(['r1']); }); it('should return the rulest with no validity or on demand', () => { @@ -124,8 +150,9 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); - expect(selectors.selectActiveRuleSets.projector(allRuleSetsArray)).toEqual(['r3']); + expect(selectors.selectActiveRuleSets.projector(rulesetsInRange)).toEqual(['r3']); }); it('should exclude the one not in validity range', () => { @@ -138,8 +165,9 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); - expect(selectors.selectActiveRuleSets.projector(allRuleSetsArray)).toEqual(['r3']); + expect(selectors.selectActiveRuleSets.projector(rulesetsInRange)).toEqual(['r3']); }); it('should not find valid rulesets', () => { @@ -152,8 +180,9 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); - expect(selectors.selectActiveRuleSets.projector(allRuleSetsArray)).toEqual([]); + expect(selectors.selectActiveRuleSets.projector(rulesetsInRange)).toEqual([]); }); it('should find a ruleset valid starting from a date in the past', () => { @@ -167,8 +196,9 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); - expect(selectors.selectActiveRuleSets.projector(allRuleSetsArray)).toEqual(['r5']); + expect(selectors.selectActiveRuleSets.projector(rulesetsInRange)).toEqual(['r5']); }); it('should find a ruleset valid until a date in the future', () => { @@ -182,8 +212,9 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); - expect(selectors.selectActiveRuleSets.projector(allRuleSetsArray)).toEqual(['r6']); + expect(selectors.selectActiveRuleSets.projector(rulesetsInRange)).toEqual(['r6']); }); it('should consider valid a ruleset with validity empty', () => { @@ -197,8 +228,9 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); - expect(selectors.selectActiveRuleSets.projector(allRuleSetsArray)).toEqual(['r7']); + expect(selectors.selectActiveRuleSets.projector(rulesetsInRange)).toEqual(['r7']); }); it('onDemand should take precedence over validity', () => { @@ -212,8 +244,9 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); - expect(selectors.selectActiveRuleSets.projector(allRuleSetsArray)).toEqual([]); + expect(selectors.selectActiveRuleSets.projector(rulesetsInRange)).toEqual([]); }); it('should select the linked components rulesets', () => { @@ -227,11 +260,13 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); + const componentsWithRulesets = { [computeItemIdentifier('mycomponent', '@mylibrary')]: [r2.id, r8.id], [computeItemIdentifier('mycomponent2', '@mylibrary')]: [r8.id] }; - expect(selectors.selectRuleSetLinkComponents.projector(allRuleSetsArray)).toEqual(componentsWithRulesets); + expect(selectors.selectRuleSetLinkComponents.projector(rulesetsInRange)).toEqual(componentsWithRulesets); }); it('should select the map of rulesets linked components', () => { @@ -245,6 +280,8 @@ describe('RuleSets Selector tests', () => { requestIds: [] }; const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); + const comp2 = computeItemIdentifier('mycomponent2', '@mylibrary'); const comp = computeItemIdentifier('mycomponent', '@mylibrary'); const componentsWithRulesets = { @@ -253,7 +290,56 @@ describe('RuleSets Selector tests', () => { [r8.id]: [comp, comp2] } }; - expect(selectors.selectComponentsLinkedToRuleset.projector(allRuleSetsArray)).toEqual(componentsWithRulesets); + expect(selectors.selectComponentsLinkedToRuleset.projector(rulesetsInRange)).toEqual(componentsWithRulesets); + }); + + it('should filter out the linked components rulesets outside the validity range', () => { + const state: RulesetsState = { + ids: [r9.id], + entities: { + 'r9': r9 + }, + requestIds: [] + }; + const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); + + expect(selectors.selectRuleSetLinkComponents.projector(rulesetsInRange)).toEqual({}); + }); + + it('should filter out the map of rulesets linked components outside the validity range', () => { + const state: RulesetsState = { + ids: [r9.id], + entities: { + 'r9': r9 + }, + requestIds: [] + }; + const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + const rulesetsInRange = selectors.selectRuleSetsInRange.projector(allRuleSetsArray); + + expect(selectors.selectComponentsLinkedToRuleset.projector(rulesetsInRange)).toEqual({ or: {} }); + }); + + it('selectRuleSetsInRange should filter out rulesets outside of the validity range', () => { + const state: RulesetsState = { + ids: [r1.id, r2.id, r3.id, r4.id, r5.id, r6.id, r7.id, r8.id, r9.id], + entities: { + 'r1': r1, + 'r2': r2, + 'r3': r3, + 'r4': r4, + 'r5': r5, + 'r6': r6, + 'r7': r7, + 'r8': r8, + 'r9': r9 + }, + requestIds: [] + }; + const allRuleSetsArray = selectors.selectAllRulesets.projector(state); + + expect(selectors.selectRuleSetsInRange.projector(allRuleSetsArray)).toEqual([r1, r2, r3, r5, r6, r7, r8]); }); }); diff --git a/packages/@o3r/rules-engine/src/stores/rulesets/rulesets.selectors.ts b/packages/@o3r/rules-engine/src/stores/rulesets/rulesets.selectors.ts index 26745ece9b..6154df5da3 100644 --- a/packages/@o3r/rules-engine/src/stores/rulesets/rulesets.selectors.ts +++ b/packages/@o3r/rules-engine/src/stores/rulesets/rulesets.selectors.ts @@ -31,18 +31,11 @@ export const selectRulesetsStorePendingStatus = createSelector(selectRulesetsSta const isValidDate = (d: any) => !isNaN(d) && d instanceof Date; /** - * Returns only the rulesets ids which are not onDemand and in the validity range - * OnDemand takes precedence over validity range. - * Only if the ruleset is NOT onDemand (this means it is taken into consideration for the runs), the validity is checked + * Returns the rulesets which are in the validity range, if provided */ -export const selectActiveRuleSets = createSelector( +export const selectRuleSetsInRange = createSelector( selectAllRulesets, (ruleSets) => ruleSets.filter((ruleSet: Ruleset) => { - - if (ruleSet.linkedComponents?.or?.length || ruleSet.linkedComponent) { - return false; - } - const validity = ruleSet.validityRange; if (!validity || !validity.from && !validity.to) { return true; @@ -62,13 +55,22 @@ export const selectActiveRuleSets = createSelector( if (from) { return from.getTime() <= time; } - return to && to.getTime() >= time; - }).map((ruleSet: Ruleset) => ruleSet.id)); + }) +); -/* +/** + * Returns the rulesets ids which are not onDemand and in the validity range + */ +export const selectActiveRuleSets = createSelector( + selectRuleSetsInRange, + (ruleSets) => ruleSets + .filter((ruleSet: Ruleset) => (!(ruleSet.linkedComponents?.or?.length || ruleSet.linkedComponent))) + .map((ruleSet: Ruleset) => ruleSet.id)); + +/** * Assign rulesetId to a component - * @deprecated; It will be rmeoved in v12 with the selector using it + * @deprecated It will be removed in v12 with the selector using it */ function linkRulesetToComponent(compName: string, library: string, ruleSetId: string, acc: Record = {}) { const configName = computeItemIdentifier(compName, library); @@ -89,7 +91,7 @@ function linkComponentToRuleset(compName: string, library: string, ruleSetId: st * @deprecated use {@link selectComponentsLinkedToRuleset} instead. It will be removed in v12 */ export const selectRuleSetLinkComponents = createSelector( - selectAllRulesets, + selectRuleSetsInRange, (ruleSets) => ruleSets .reduce((acc: Record, ruleSet: Ruleset) => { @@ -113,7 +115,7 @@ export const selectRuleSetLinkComponents = createSelector( * Select the map of ruleSets to activate based on linked components */ export const selectComponentsLinkedToRuleset = createSelector( - selectAllRulesets, + selectRuleSetsInRange, (ruleSets) => ruleSets .reduce((acc: {or: {[key: string]: string[]}}, ruleSet: Ruleset) => { @@ -132,4 +134,3 @@ export const selectComponentsLinkedToRuleset = createSelector( return acc; }, {or: {}}) ); - diff --git a/packages/@o3r/rules-engine/testing/mocks/onerulesetvalid-onerule-nocond.mock.ts b/packages/@o3r/rules-engine/testing/mocks/onerulesetvalid-onerule-nocond.mock.ts index 2a0425a2b3..5b06479efb 100644 --- a/packages/@o3r/rules-engine/testing/mocks/onerulesetvalid-onerule-nocond.mock.ts +++ b/packages/@o3r/rules-engine/testing/mocks/onerulesetvalid-onerule-nocond.mock.ts @@ -31,6 +31,9 @@ export const jsonOneRulesetValidOneRuleNoCond : {ruleSets: Ruleset[]} = { { 'id': 'e5th46e84-5e4th-54eth65seth46se8th2', 'name': 'the second ruleset', + 'validityRange': { + 'to': '2025-07-23T18:25:43.511Z' + }, 'linkedComponent': { 'library': '@otter/comps', 'name': 'TestComponent' @@ -50,7 +53,7 @@ export const jsonOneRulesetValidOneRuleNoCond : {ruleSets: Ruleset[]} = { 'elementType': 'ACTION', 'actionType': 'UPDATE_LOCALISATION', 'key': 'my.ssci.loc.key', - 'value': 'my.custom.ssci.loc.key2' + 'value': 'my.custom.ssci.loc.key3' } ], 'failureElements': [] @@ -64,6 +67,10 @@ export const jsonOneRulesetValidOneRuleNoCond : {ruleSets: Ruleset[]} = { 'validityRange': { 'from': '2100-07-23T18:25:43.511Z' }, + 'linkedComponent': { + 'library': '@otter/comps', + 'name': 'TestComponent2' + }, 'rules': [ { 'id': '6e8t54h6s4e-6erth46sre8th4-d46t8s13t5j1',