diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index c258e2372..9bb0e3b28 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -696,19 +696,21 @@ "rules.simulate": "Simulate", "rules.simulateTooltip": "Simulate this rules using the last 100 events.", "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", - "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", - "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", - "rules.simulation.errorConditionDoesNotMatch": "Condition does not match to the trigger.", - "rules.simulation.errorDisabled": "Rule is dissabled.", + "rules.simulation.actionExecuted": "Job is taken from the queue and executed.", + "rules.simulation.errorConditionDoesNotMatch": "STOP: Javescript expressions in trigger do not match to the event.", + "rules.simulation.errorConditionPrecheckDoesNotMatch": "STOP: Condition in trigger does not match to the event.", + "rules.simulation.errorDisabled": "STOP: Rule is disabled.", "rules.simulation.errorFailed": "Internal Error.", - "rules.simulation.errorFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", - "rules.simulation.errorNoAction": "Action type is obsolete and has been removed.", - "rules.simulation.errorNoTrigger": "Trigger type is obsolete and has been removed.", - "rules.simulation.errorTooOld": "Event is too old.", - "rules.simulation.errorWrongEvent": "Event does not match to the trigger.", - "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorFromRule": "STOP: Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorNoAction": "STOP: Action type is obsolete and has been removed.", + "rules.simulation.errorNoTrigger": "STOP: Trigger type is obsolete and has been removed.", + "rules.simulation.errorTooOld": "STOP: Event is too old.", + "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", + "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matches to the conditions and javascript expressions in the tigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", + "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", "rules.simulator": "Simulator", "rules.stop": "Rule will stop soon.", "rules.triggerConfirmText": "Do you really want to trigger the rule?", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 6e90dbbce..2ef8f198c 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -696,19 +696,21 @@ "rules.simulate": "Simulate", "rules.simulateTooltip": "Simulate this rules using the last 100 events.", "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", - "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", - "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", - "rules.simulation.errorConditionDoesNotMatch": "Condition does not match to the trigger.", - "rules.simulation.errorDisabled": "Rule is dissabled.", + "rules.simulation.actionExecuted": "Job is taken from the queue and executed.", + "rules.simulation.errorConditionDoesNotMatch": "STOP: Javescript expressions in trigger do not match to the event.", + "rules.simulation.errorConditionPrecheckDoesNotMatch": "STOP: Condition in trigger does not match to the event.", + "rules.simulation.errorDisabled": "STOP: Rule is disabled.", "rules.simulation.errorFailed": "Internal Error.", - "rules.simulation.errorFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", - "rules.simulation.errorNoAction": "Action type is obsolete and has been removed.", - "rules.simulation.errorNoTrigger": "Trigger type is obsolete and has been removed.", - "rules.simulation.errorTooOld": "Event is too old.", - "rules.simulation.errorWrongEvent": "Event does not match to the trigger.", - "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorFromRule": "STOP: Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorNoAction": "STOP: Action type is obsolete and has been removed.", + "rules.simulation.errorNoTrigger": "STOP: Trigger type is obsolete and has been removed.", + "rules.simulation.errorTooOld": "STOP: Event is too old.", + "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", + "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matches to the conditions and javascript expressions in the tigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", + "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", "rules.simulator": "Simulator", "rules.stop": "La regola si fermerà al più presto.", "rules.triggerConfirmText": "Sei sicuro che voler attivare la regola?", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index ddc0e5fd4..649fff492 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -696,19 +696,21 @@ "rules.simulate": "Simulate", "rules.simulateTooltip": "Simulate this rules using the last 100 events.", "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", - "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", - "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", - "rules.simulation.errorConditionDoesNotMatch": "Condition does not match to the trigger.", - "rules.simulation.errorDisabled": "Rule is dissabled.", + "rules.simulation.actionExecuted": "Job is taken from the queue and executed.", + "rules.simulation.errorConditionDoesNotMatch": "STOP: Javescript expressions in trigger do not match to the event.", + "rules.simulation.errorConditionPrecheckDoesNotMatch": "STOP: Condition in trigger does not match to the event.", + "rules.simulation.errorDisabled": "STOP: Rule is disabled.", "rules.simulation.errorFailed": "Internal Error.", - "rules.simulation.errorFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", - "rules.simulation.errorNoAction": "Action type is obsolete and has been removed.", - "rules.simulation.errorNoTrigger": "Trigger type is obsolete and has been removed.", - "rules.simulation.errorTooOld": "Event is too old.", - "rules.simulation.errorWrongEvent": "Event does not match to the trigger.", - "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorFromRule": "STOP: Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorNoAction": "STOP: Action type is obsolete and has been removed.", + "rules.simulation.errorNoTrigger": "STOP: Trigger type is obsolete and has been removed.", + "rules.simulation.errorTooOld": "STOP: Event is too old.", + "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", + "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matches to the conditions and javascript expressions in the tigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", + "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", "rules.simulator": "Simulator", "rules.stop": "Regel stopt binnenkort.", "rules.triggerConfirmText": "Wil je echt de regel activeren?", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index faa05c533..8dc6ecaeb 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -696,19 +696,21 @@ "rules.simulate": "模拟", "rules.simulateTooltip": "使用最近 100 个事件模拟此规则。", "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", - "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", - "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", - "rules.simulation.errorConditionDoesNotMatch": "Condition does not match to the trigger.", - "rules.simulation.errorDisabled": "Rule is dissabled.", + "rules.simulation.actionExecuted": "Job is taken from the queue and executed.", + "rules.simulation.errorConditionDoesNotMatch": "STOP: Javescript expressions in trigger do not match to the event.", + "rules.simulation.errorConditionPrecheckDoesNotMatch": "STOP: Condition in trigger does not match to the event.", + "rules.simulation.errorDisabled": "STOP: Rule is disabled.", "rules.simulation.errorFailed": "Internal Error.", - "rules.simulation.errorFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", - "rules.simulation.errorNoAction": "Action type is obsolete and has been removed.", - "rules.simulation.errorNoTrigger": "Trigger type is obsolete and has been removed.", - "rules.simulation.errorTooOld": "Event is too old.", - "rules.simulation.errorWrongEvent": "Event does not match to the trigger.", - "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorFromRule": "STOP: Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorNoAction": "STOP: Action type is obsolete and has been removed.", + "rules.simulation.errorNoTrigger": "STOP: Trigger type is obsolete and has been removed.", + "rules.simulation.errorTooOld": "STOP: Event is too old.", + "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", + "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matches to the conditions and javascript expressions in the tigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", + "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", "rules.simulator": "模拟器", "rules.stop": "规则很快就会停止。", "rules.triggerConfirmText": "你真的要触发规则吗?", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index c258e2372..9bb0e3b28 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -696,19 +696,21 @@ "rules.simulate": "Simulate", "rules.simulateTooltip": "Simulate this rules using the last 100 events.", "rules.simulation.actionCreated": "Job is created from the enriched event and action and added to a job queue.", - "rules.simulation.actionExecuted": "Job will be taken from the queue and executed.", - "rules.simulation.conditionEvaluated": "Event is evaluated, whether it matches the conditions in the tigger. ", - "rules.simulation.errorConditionDoesNotMatch": "Condition does not match to the trigger.", - "rules.simulation.errorDisabled": "Rule is dissabled.", + "rules.simulation.actionExecuted": "Job is taken from the queue and executed.", + "rules.simulation.errorConditionDoesNotMatch": "STOP: Javescript expressions in trigger do not match to the event.", + "rules.simulation.errorConditionPrecheckDoesNotMatch": "STOP: Condition in trigger does not match to the event.", + "rules.simulation.errorDisabled": "STOP: Rule is disabled.", "rules.simulation.errorFailed": "Internal Error.", - "rules.simulation.errorFromRule": "Event has been created from another rule and will be skipped to prevent endless loops.", - "rules.simulation.errorNoAction": "Action type is obsolete and has been removed.", - "rules.simulation.errorNoTrigger": "Trigger type is obsolete and has been removed.", - "rules.simulation.errorTooOld": "Event is too old.", - "rules.simulation.errorWrongEvent": "Event does not match to the trigger.", - "rules.simulation.errorWrongEventForTrigger": "Event does not match to the trigger.", + "rules.simulation.errorFromRule": "STOP: Event has been created from another rule and will be skipped to prevent endless loops.", + "rules.simulation.errorNoAction": "STOP: Action type is obsolete and has been removed.", + "rules.simulation.errorNoTrigger": "STOP: Trigger type is obsolete and has been removed.", + "rules.simulation.errorTooOld": "STOP: Event is too old.", + "rules.simulation.errorWrongEvent": "STOP: Event does not match to the trigger.", + "rules.simulation.errorWrongEventForTrigger": "STOP: Event does not match to the trigger.", + "rules.simulation.eventConditionEvaluated": "Enriched event is evaluated, whether it matches to the conditions and javascript expressions in the tigger.", "rules.simulation.eventEnriched": "Event is enriched with additional data", "rules.simulation.eventQueried": "Event is queried from the database", + "rules.simulation.eventTriggerChecked": "Event is tested to see if it matchs to the trigger and the basic conditions.", "rules.simulator": "Simulator", "rules.stop": "Rule will stop soon.", "rules.triggerConfirmText": "Do you really want to trigger the rule?", diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs index 115517830..20f0d565f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs @@ -26,4 +26,4 @@ namespace Squidex.Domain.Apps.Core.HandleRules Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job); } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs index fad228fca..68169f059 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/JobResult.cs @@ -18,6 +18,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules SkipReason = SkipReason.ConditionDoesNotMatch }; + public static readonly JobResult ConditionPrecheckDoesNotMatch = new JobResult + { + SkipReason = SkipReason.ConditionPrecheckDoesNotMatch + }; + public static readonly JobResult Disabled = new JobResult { SkipReason = SkipReason.Disabled @@ -61,13 +66,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules public SkipReason SkipReason { get; init; } - public static JobResult Failed(Exception error, EnrichedEvent? enrichedEvent = null, RuleJob? job = null) + public static JobResult Failed(Exception exception, EnrichedEvent? enrichedEvent = null, RuleJob? job = null) { return new JobResult { Job = job, EnrichedEvent = enrichedEvent, - EnrichmentError = error, + EnrichmentError = exception, SkipReason = SkipReason.Failed }; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs index f74d27aa9..7499e760a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleContext.cs @@ -18,6 +18,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules public Rule Rule { get; init; } - public bool IgnoreStale { get; init; } + public bool IncludeSkipped { get; init; } + + public bool IncludeStale { get; init; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index c4c6c6bce..edfd68755 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -149,12 +149,22 @@ namespace Squidex.Domain.Apps.Core.HandleRules { try { + var skipReason = SkipReason.None; + var rule = context.Rule; if (!rule.IsEnabled) { - jobs.Add(JobResult.Disabled); - return; + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) + { + skipReason |= SkipReason.Disabled; + } + else + { + jobs.Add(JobResult.Disabled); + return; + } } if (@event.Payload is not AppEvent) @@ -167,8 +177,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules if (typed.Payload.FromRule) { - jobs.Add(JobResult.FromRule); - return; + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) + { + skipReason |= SkipReason.FromRule; + } + else + { + jobs.Add(JobResult.FromRule); + return; + } } var actionType = rule.Action.GetType(); @@ -198,17 +216,32 @@ namespace Squidex.Domain.Apps.Core.HandleRules @event.Headers.Timestamp() : now; - if (context.IgnoreStale && eventTime.Plus(Constants.StaleTime) < now) + if (!context.IncludeStale && eventTime.Plus(Constants.StaleTime) < now) { - jobs.Add(JobResult.TooOld); - return; + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) + { + skipReason |= SkipReason.TooOld; + } + else + { + jobs.Add(JobResult.TooOld); + return; + } } - var skipReason = SkipReason.None; - if (!triggerHandler.Trigger(typed, context)) { - skipReason = SkipReason.ConditionDoesNotMatch; + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) + { + skipReason |= SkipReason.ConditionPrecheckDoesNotMatch; + } + else + { + jobs.Add(JobResult.ConditionPrecheckDoesNotMatch); + return; + } } await foreach (var enrichedEvent in triggerHandler.CreateEnrichedEventsAsync(typed, context, ct)) @@ -224,12 +257,27 @@ namespace Squidex.Domain.Apps.Core.HandleRules if (!triggerHandler.Trigger(enrichedEvent, context)) { - skipReason = SkipReason.ConditionDoesNotMatch; + // For the simulation we want to proceed as much as possible. + if (context.IncludeSkipped) + { + skipReason |= SkipReason.ConditionDoesNotMatch; + } + else + { + jobs.Add(JobResult.ConditionDoesNotMatch); + return; + } } var job = await CreateJobAsync(actionHandler, enrichedEvent, context, now); - jobs.Add(job with { SkipReason = skipReason }); + // If the conditions matchs, we can skip creating a new object and save a few allocation.s + if (skipReason != SkipReason.None) + { + job = job with { SkipReason = skipReason }; + } + + jobs.Add(job); } catch (Exception ex) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs index d58055981..129a82167 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs @@ -5,19 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; + namespace Squidex.Domain.Apps.Core.HandleRules { + [Flags] public enum SkipReason { None, - ConditionDoesNotMatch, - Disabled, - WrongEvent, - Failed, - FromRule, - NoAction, - NoTrigger, - TooOld, - WrongEventForTrigger + ConditionDoesNotMatch = 1 << 0, + ConditionPrecheckDoesNotMatch = 1 << 1, + Disabled = 1 << 2, + Failed = 1 << 3, + FromRule = 1 << 4, + NoAction = 1 << 5, + NoTrigger = 1 << 6, + TooOld = 1 << 7, + WrongEvent = 1 << 8, + WrongEventForTrigger = 1 << 9 } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index bae797990..b9072193f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -54,7 +54,6 @@ namespace Squidex.Domain.Apps.Entities.Rules { Rule = rule, RuleId = ruleId, - IgnoreStale = false }; var jobs = ruleService.CreateJobsAsync(@event, ruleContext); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs index 0f9bc210c..34ddd615b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs @@ -41,7 +41,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { Guard.NotNull(rule, nameof(rule)); - var context = GetContext(rule); + var context = new RuleContext + { + AppId = rule.AppId, + Rule = rule.RuleDef, + RuleId = rule.Id, + IncludeSkipped = true, + IncludeStale = true + }; var simulatedEvents = new List(MaxSimulatedEvents); @@ -53,6 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner if (@event?.Payload is AppEvent appEvent) { + // Also create jobs for rules with failing conditions because we want to show them in th table. await foreach (var result in ruleService.CreateJobsAsync(@event, context, ct)) { var eventName = result.Job?.EventName; @@ -120,8 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { AppId = rule.AppId, Rule = rule.RuleDef, - RuleId = rule.Id, - IgnoreStale = false + RuleId = rule.Id }; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs index 628a2be81..6d750a48d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs @@ -167,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner AppId = rule.AppId, Rule = rule.RuleDef, RuleId = rule.Id, - IgnoreStale = true + IncludeStale = true }; if (currentState.RunFromSnapshots && ruleService.CanCreateSnapshotEvents(context)) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs index 614a737fd..f78fa89b8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/SimulatedRuleEventDto.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Entities.Rules.Runner; @@ -50,11 +52,24 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models /// The reason why the event has been skipped. /// [Required] - public SkipReason SkipReason { get; set; } + public List SkipReasons { get; set; } public static SimulatedRuleEventDto FromSimulatedRuleEvent(SimulatedRuleEvent ruleEvent) { - return SimpleMapper.Map(ruleEvent, new SimulatedRuleEventDto()); + var result = SimpleMapper.Map(ruleEvent, new SimulatedRuleEventDto + { + SkipReasons = new List() + }); + + foreach (var reason in Enum.GetValues()) + { + if (reason != SkipReason.None && ruleEvent.SkipReason.HasFlag(reason)) + { + result.SkipReasons.Add(reason); + } + } + + return result; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 59683bd43..402f96456 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -96,15 +96,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_calculate_event_name_from_trigger_handler() { - var @event = new ContentCreated(); + var eventEnvelope = new ContentCreated(); - A.CallTo(() => ruleTriggerHandler.Handles(@event)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.GetName(@event)) + A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) .Returns("custom-name"); - var name = sut.GetName(@event); + var name = sut.GetName(eventEnvelope); Assert.Equal("custom-name", name); } @@ -112,42 +112,44 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_calculate_default_name_if_trigger_handler_returns_no_name() { - var @event = new ContentCreated(); + var eventEnvelope = new ContentCreated(); - A.CallTo(() => ruleTriggerHandler.Handles(@event)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.GetName(@event)) + A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) .Returns(null); - var name = sut.GetName(@event); + var name = sut.GetName(eventEnvelope); Assert.Equal("ContentCreated", name); - A.CallTo(() => ruleTriggerHandler.GetName(@event)) + A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) .MustHaveHappened(); } [Fact] public void Should_calculate_default_name_if_trigger_handler_cannot_not_handle_event() { - var @event = new ContentCreated(); + var eventEnvelope = new ContentCreated(); - A.CallTo(() => ruleTriggerHandler.Handles(@event)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope)) .Returns(false); - var name = sut.GetName(@event); + var name = sut.GetName(eventEnvelope); Assert.Equal("ContentCreated", name); - A.CallTo(() => ruleTriggerHandler.GetName(@event)) + A.CallTo(() => ruleTriggerHandler.GetName(eventEnvelope)) .MustNotHaveHappened(); } [Fact] public void Should_not_run_from_snapshots_if_no_trigger_handler_registered() { - var result = sut.CanCreateSnapshotEvents(RuleInvalidTrigger()); + var context = RuleInvalidTrigger(); + + var result = sut.CanCreateSnapshotEvents(context); Assert.False(result); } @@ -155,10 +157,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_not_run_from_snapshots_if_trigger_handler_does_not_support_it() { + var context = Rule(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) .Returns(false); - var result = sut.CanCreateSnapshotEvents(Rule()); + var result = sut.CanCreateSnapshotEvents(context); Assert.False(result); } @@ -166,10 +170,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public void Should_run_from_snapshots_if_trigger_handler_does_support_it() { + var context = Rule(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) .Returns(true); - var result = sut.CanCreateSnapshotEvents(Rule()); + var result = sut.CanCreateSnapshotEvents(context); Assert.True(result); } @@ -177,10 +183,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_from_snapshots_if_trigger_handler_does_not_support_it() { + var context = Rule(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) .Returns(false); - var jobs = await sut.CreateSnapshotJobsAsync(Rule()).ToListAsync(); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); Assert.Empty(jobs); @@ -191,10 +199,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_from_snapshots_if_rule_disabled() { + var context = Rule(disable: true); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) .Returns(true); - var jobs = await sut.CreateSnapshotJobsAsync(Rule(disable: true)).ToListAsync(); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); Assert.Empty(jobs); @@ -205,10 +215,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_from_snapshots_if_no_trigger_handler_registered() { + var context = RuleInvalidTrigger(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) .Returns(true); - var jobs = await sut.CreateSnapshotJobsAsync(RuleInvalidTrigger()).ToListAsync(); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); Assert.Empty(jobs); @@ -219,10 +231,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_not_create_job_from_snapshots_if_no_action_handler_registered() { + var context = RuleInvalidAction(); + A.CallTo(() => ruleTriggerHandler.CanCreateSnapshotEvents) .Returns(true); - var jobs = await sut.CreateSnapshotJobsAsync(RuleInvalidAction()).ToListAsync(); + var jobs = await sut.CreateSnapshotJobsAsync(context).ToListAsync(); Assert.Empty(jobs); @@ -276,85 +290,118 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal(2, jobs.Count(x => x.Job == null && x.EnrichmentError != null)); } - [Fact] - public async Task Should_create_debug_rob_if_rule_disabled() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_create_debug_job_for_invalid_event(bool includeSkipped) { - var @event = Envelope.Create(new ContentCreated()); + var context = Rule(includeSkipped: includeSkipped); - var result = await sut.CreateJobsAsync(@event, Rule(disable: true)).SingleAsync(); + var eventEnvelope = CreateEnvelope(new InvalidEvent()); - Assert.Equal(SkipReason.Disabled, result.SkipReason); + var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + + Assert.Equal(SkipReason.WrongEvent, result.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); } - [Fact] - public async Task Should_create_debug_job_for_invalid_event() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_create_debug_job_if_no_trigger_handler_registered(bool includeSkipped) { - var @event = Envelope.Create(new InvalidEvent()); + var context = RuleInvalidTrigger(includeSkipped); - var result = await sut.CreateJobsAsync(@event, Rule()).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - Assert.Equal(SkipReason.WrongEvent, result.SkipReason); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + + Assert.Equal(SkipReason.NoTrigger, job.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); } - [Fact] - public async Task Should_create_debug_job_if_no_trigger_handler_registered() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_create_debug_job_if_trigger_handler_does_not_handle_event(bool includeSkipped) { - var @event = Envelope.Create(new ContentCreated()); + var context = Rule(includeSkipped: includeSkipped); - var job = await sut.CreateJobsAsync(@event, RuleInvalidTrigger()).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - Assert.Equal(SkipReason.NoTrigger, job.SkipReason); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + + Assert.Equal(SkipReason.WrongEventForTrigger, job.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); } - [Fact] - public async Task Should_create_debug_job_if_trigger_handler_does_not_handle_event() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Should_create_debug_job_if_no_action_handler_registered(bool includeSkipped) { - var @event = Envelope.Create(new ContentCreated()); + var context = RuleInvalidAction(includeSkipped); - var job = await sut.CreateJobsAsync(@event, Rule()).SingleAsync(); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - Assert.Equal(SkipReason.WrongEventForTrigger, job.SkipReason); + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); + + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + + Assert.Equal(SkipReason.NoAction, job.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_create_debug_job_if_no_action_handler_registered() + public async Task Should_create_debug_job_if_rule_disabled() { - var @event = Envelope.Create(new ContentCreated()); + var context = Rule(disable: true); - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) - .Returns(true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - var job = await sut.CreateJobsAsync(@event, RuleInvalidAction()).SingleAsync(); + var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - Assert.Equal(SkipReason.NoAction, job.SkipReason); + Assert.Equal(SkipReason.Disabled, result.SkipReason); A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); } + [Fact] + public async Task Should_create_job_if_rule_disabled_and_skipped_included() + { + var context = Rule(disable: true, includeSkipped: true); + + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); + + var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + + AssertJob(eventEnriched, result, SkipReason.Disabled); + } + [Fact] public async Task Should_create_debug_job_if_too_old() { - var @event = + var context = Rule(); + + var eventEnvelope = Envelope.Create(new ContentCreated()) .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) .Returns(true); - var job = await sut.CreateJobsAsync(@event, Rule(ignoreStale: true)).SingleAsync(); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); Assert.Equal(SkipReason.TooOld, job.SkipReason); @@ -363,29 +410,33 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules } [Fact] - public async Task Should_create_job_if_too_old_but_stale_events_are_not_ignored() + public async Task Should_create_job_if_too_old_but_stale_events_are_included() { - var context = Rule(ignoreStale: false); + var context = Rule(includeStale: true); - var @event = + var eventEnvelope = Envelope.Create(new ContentCreated()) .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) - .Returns(true); + var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A>._, context)) - .Returns(true); + AssertJob(eventEnriched, result, SkipReason.None); + } - A.CallTo(() => ruleTriggerHandler.Trigger(A._, context)) - .Returns(true); + [Fact] + public async Task Should_create_job_if_too_old_but_skipped_are_included() + { + var context = Rule(includeSkipped: true); - var jobs = await sut.CreateJobsAsync(@event, context).ToListAsync(); + var eventEnvelope = + Envelope.Create(new ContentCreated()) + .SetTimestamp(clock.GetCurrentInstant().Minus(Duration.FromDays(3))); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - Assert.Empty(jobs); + var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) - .MustHaveHappened(); + AssertJob(eventEnriched, result, SkipReason.TooOld); } [Fact] @@ -393,9 +444,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var context = Rule(); - var @event = Envelope.Create(new ContentCreated { FromRule = true }); + var eventEnvelope = CreateEnvelope(new ContentCreated { FromRule = true }); - var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); Assert.Equal(SkipReason.FromRule, job.SkipReason); @@ -406,28 +457,57 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .MustNotHaveHappened(); } + [Fact] + public async Task Should_job_if_event_created_by_rule_but_skipped_are_included() + { + var context = Rule(includeSkipped: true); + + var eventEnvelope = CreateEnvelope(new ContentCreated { FromRule = true }); + var eventEnriched = SetupFullFlow(context, eventEnvelope); + + var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + + AssertJob(eventEnriched, result, SkipReason.FromRule); + } + [Fact] public async Task Should_create_debug_job_if_not_triggered_with_precheck() { var context = Rule(); - var enrichedEvent = new EnrichedContentEvent { AppId = appId }; + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = new EnrichedContentEvent { AppId = appId }; - var @event = Envelope.Create(new ContentCreated()); - - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) .Returns(false); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) - .Returns(new List { enrichedEvent }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(new List { eventEnriched }.ToAsyncEnumerable()); - var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - Assert.Equal(SkipReason.ConditionDoesNotMatch, job.SkipReason); - Assert.Equal(enrichedEvent, job.EnrichedEvent); + Assert.Equal(SkipReason.ConditionPrecheckDoesNotMatch, job.SkipReason); + Assert.Null(job.EnrichedEvent); + Assert.Null(job.Job); + } + + [Fact] + public async Task Should_create_job_if_not_triggered_with_precheck_but_skipped_are_included() + { + var context = Rule(includeSkipped: true); + + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); + + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(false); + + var result = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + + AssertJob(eventEnriched, result, SkipReason.ConditionPrecheckDoesNotMatch); } [Fact] @@ -435,15 +515,15 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var context = Rule(); - var @event = Envelope.Create(new ContentCreated()); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) .Throws(new InvalidOperationException()); - var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); Assert.Equal(SkipReason.Failed, job.SkipReason); } @@ -453,18 +533,18 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { var context = Rule(); - var @event = Envelope.Create(new ContentCreated()); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) .Returns(AsyncEnumerable.Empty()); - var job = await sut.CreateJobsAsync(@event, context).ToListAsync(); + var job = await sut.CreateJobsAsync(eventEnvelope, context).ToListAsync(); Assert.Empty(job); } @@ -472,51 +552,62 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_create_debug_job_if_not_triggered() { - var context = Rule(); - - var enrichedEvent = new EnrichedContentEvent { AppId = appId }; + var context = Rule(includeSkipped: true); - var @event = Envelope.Create(new ContentCreated()); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = new EnrichedContentEvent { AppId = appId }; - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, context)) + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) .Returns(false); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) - .Returns(new List { enrichedEvent }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(new List { eventEnriched }.ToAsyncEnumerable()); - var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); Assert.Equal(SkipReason.ConditionDoesNotMatch, job.SkipReason); - Assert.Equal(enrichedEvent, job.EnrichedEvent); + Assert.Equal(eventEnriched, job.EnrichedEvent); } [Fact] - public async Task Should_create_debug_job_if_enrichment_failed() + public async Task Should_debug_job_if_not_triggered_but_skipped_included() { - var now = clock.GetCurrentInstant(); + var context = Rule(includeSkipped: true); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); + + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) + .Returns(false); + + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); + + AssertJob(eventEnriched, job, SkipReason.ConditionDoesNotMatch); + } + + [Fact] + public async Task Should_create_debug_job_if_enrichment_failed() + { var context = Rule(); - var @event = - Envelope.Create(new ContentCreated()) - .SetTimestamp(now); + var eventEnvelope = CreateEnvelope(new ContentCreated()); - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) .Throws(new InvalidOperationException()); - var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); Assert.Equal(SkipReason.Failed, job.SkipReason); } @@ -524,122 +615,80 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules [Fact] public async Task Should_create_job_if_triggered() { - var now = clock.GetCurrentInstant(); - var context = Rule(); - var enrichedEvent = new EnrichedContentEvent { AppId = appId }; + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - var @event = - Envelope.Create(new ContentCreated()) - .SetTimestamp(now); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) - .Returns(true); + AssertJob(eventEnriched, job, SkipReason.None); - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, context)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) - .Returns(new List { enrichedEvent }.ToAsyncEnumerable()); - - A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent, context.Rule.Action)) - .Returns((actionDescription, new ValidData { Value = 10 })); - - var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); - - AssertJob(now, enrichedEvent, job); - - A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, MatchPayload(@event))) + A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched, MatchPayload(eventEnvelope))) .MustHaveHappened(); } [Fact] public async Task Should_create_job_with_exception_if_trigger_failed() { - var now = clock.GetCurrentInstant(); - var context = Rule(); - var enrichedEvent = new EnrichedContentEvent { AppId = appId }; + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched = SetupFullFlow(context, eventEnvelope); - var @event = - Envelope.Create(new ContentCreated()) - .SetTimestamp(now); - - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent, context)) - .Returns(true); - - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) - .Returns(new List { enrichedEvent }.ToAsyncEnumerable()); - - A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent, context.Rule.Action)) + A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched, context.Rule.Action)) .Throws(new InvalidOperationException()); - var job = await sut.CreateJobsAsync(@event, context).SingleAsync(); + var job = await sut.CreateJobsAsync(eventEnvelope, context).SingleAsync(); Assert.NotNull(job.EnrichmentError); Assert.NotNull(job.Job?.ActionData); Assert.NotNull(job.Job?.Description); - Assert.Equal(enrichedEvent, job.EnrichedEvent); + Assert.Equal(eventEnriched, job.EnrichedEvent); - A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent, MatchPayload(@event))) + A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched, MatchPayload(eventEnvelope))) .MustHaveHappened(); } [Fact] public async Task Should_create_multiple_jobs_if_triggered() { - var now = clock.GetCurrentInstant(); - var context = Rule(); - var enrichedEvent1 = new EnrichedContentEvent { AppId = appId }; - var enrichedEvent2 = new EnrichedContentEvent { AppId = appId }; - - var @event = - Envelope.Create(new ContentCreated()) - .SetTimestamp(now); + var eventEnvelope = CreateEnvelope(new ContentCreated()); + var eventEnriched1 = new EnrichedContentEvent { AppId = appId }; + var eventEnriched2 = new EnrichedContentEvent { AppId = appId }; - A.CallTo(() => ruleTriggerHandler.Handles(@event.Payload)) + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(@event), context)) + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent1, context)) + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched1, context)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.Trigger(enrichedEvent2, context)) + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched2, context)) .Returns(true); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(@event), context, default)) - .Returns(new List { enrichedEvent1, enrichedEvent2 }.ToAsyncEnumerable()); + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(new List { eventEnriched1, eventEnriched2 }.ToAsyncEnumerable()); - A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent1, context.Rule.Action)) + A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched1, context.Rule.Action)) .Returns((actionDescription, new ValidData { Value = 10 })); - A.CallTo(() => ruleActionHandler.CreateJobAsync(enrichedEvent2, context.Rule.Action)) + A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched2, context.Rule.Action)) .Returns((actionDescription, new ValidData { Value = 10 })); - var jobs = await sut.CreateJobsAsync(@event, context, default).ToListAsync(); + var jobs = await sut.CreateJobsAsync(eventEnvelope, context, default).ToListAsync(); - AssertJob(now, enrichedEvent1, jobs[0]); - AssertJob(now, enrichedEvent2, jobs[1]); + AssertJob(eventEnriched1, jobs[0], SkipReason.None); + AssertJob(eventEnriched2, jobs[1], SkipReason.None); - A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent1, MatchPayload(@event))) + A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched1, MatchPayload(eventEnvelope))) .MustHaveHappened(); - A.CallTo(() => eventEnricher.EnrichAsync(enrichedEvent2, MatchPayload(@event))) + A.CallTo(() => eventEnricher.EnrichAsync(eventEnriched2, MatchPayload(eventEnvelope))) .MustHaveHappened(); } @@ -700,27 +749,51 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal(ex, result.Result.Exception); } - private RuleContext RuleInvalidAction() + private EnrichedContentEvent SetupFullFlow(RuleContext context, Envelope eventEnvelope) where T : AppEvent + { + var eventEnriched = new EnrichedContentEvent { AppId = appId }; + + A.CallTo(() => ruleTriggerHandler.Handles(eventEnvelope.Payload)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.Trigger(MatchPayload(eventEnvelope), context)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.Trigger(eventEnriched, context)) + .Returns(true); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(MatchPayload(eventEnvelope), context, default)) + .Returns(new List { eventEnriched }.ToAsyncEnumerable()); + + A.CallTo(() => ruleActionHandler.CreateJobAsync(eventEnriched, context.Rule.Action)) + .Returns((actionDescription, new ValidData { Value = 10 })); + + return eventEnriched; + } + + private RuleContext RuleInvalidAction(bool includeSkipped = false) { return new RuleContext { AppId = appId, Rule = new Rule(new ContentChangedTriggerV2(), new InvalidAction()), - RuleId = ruleId + RuleId = ruleId, + IncludeSkipped = includeSkipped }; } - private RuleContext RuleInvalidTrigger() + private RuleContext RuleInvalidTrigger(bool includeSkipped = false) { return new RuleContext { AppId = appId, Rule = new Rule(new InvalidTrigger(), new ValidAction()), - RuleId = ruleId + RuleId = ruleId, + IncludeSkipped = includeSkipped }; } - private RuleContext Rule(bool disable = false, bool ignoreStale = true) + private RuleContext Rule(bool disable = false, bool includeStale = false, bool includeSkipped = false) { var rule = new Rule(new ContentChangedTriggerV2(), new ValidAction()); @@ -734,21 +807,31 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules AppId = appId, Rule = rule, RuleId = ruleId, - IgnoreStale = ignoreStale + IncludeStale = includeStale, + IncludeSkipped = includeSkipped }; } - private static Envelope MatchPayload(Envelope @event) + private Envelope CreateEnvelope(T @event) where T : class, IEvent + { + return Envelope.Create(@event).SetTimestamp(clock.GetCurrentInstant()); + } + + private static Envelope MatchPayload(Envelope eventEnvelope) { - return A>.That.Matches(x => x.Payload == @event.Payload); + return A>.That.Matches(x => x.Payload == eventEnvelope.Payload); } - private void AssertJob(Instant now, EnrichedContentEvent enrichedEvent, JobResult result) + private void AssertJob(EnrichedContentEvent eventEnriched, JobResult result, SkipReason skipped) { + var now = clock.GetCurrentInstant(); + var job = result.Job!; - Assert.Equal(enrichedEvent, result.EnrichedEvent); - Assert.Equal(enrichedEvent.AppId.Id, job.AppId); + Assert.Equal(skipped, result.SkipReason); + + Assert.Equal(eventEnriched, result.EnrichedEvent); + Assert.Equal(eventEnriched.AppId.Id, job.AppId); Assert.Equal(actionData, job.ActionData); Assert.Equal(actionName, job.ActionName); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index edb379e70..95dd2eadb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Rules Created = now }; - A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule.RuleDef), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) .Returns(new List { new JobResult() }.ToAsyncEnumerable()); await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); @@ -108,7 +108,7 @@ namespace Squidex.Domain.Apps.Entities.Rules Created = now }; - A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule.RuleDef), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) .Returns(new List { new JobResult { Job = job, SkipReason = SkipReason.TooOld } }.ToAsyncEnumerable()); await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); @@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Rules Created = now }; - A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule.RuleDef), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule), default)) .Returns(new List { new JobResult { Job = job } }.ToAsyncEnumerable()); await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); @@ -179,10 +179,10 @@ namespace Squidex.Domain.Apps.Entities.Rules A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) .Returns(new List { rule1, rule2 }); - A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule1.RuleDef), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule1), default)) .Returns(new List { new JobResult { Job = job1 } }.ToAsyncEnumerable()); - A.CallTo(() => ruleService.CreateJobsAsync(@event, A.That.Matches(x => x.Rule == rule2.RuleDef), default)) + A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule2), default)) .Returns(new List().ToAsyncEnumerable()); } @@ -192,5 +192,14 @@ namespace Squidex.Domain.Apps.Entities.Rules return new RuleEntity { RuleDef = rule, Id = DomainId.NewGuid() }; } + + private static RuleContext MatchingContext(RuleEntity rule) + { + // These two properties must not be set to true for performance reasons. + return A.That.Matches(x => + x.Rule == rule.RuleDef && + !x.IncludeSkipped && + !x.IncludeStale); + } } } diff --git a/frontend/app/features/rules/declarations.ts b/frontend/app/features/rules/declarations.ts index 534009fff..78bfff2ef 100644 --- a/frontend/app/features/rules/declarations.ts +++ b/frontend/app/features/rules/declarations.ts @@ -11,6 +11,7 @@ export * from './pages/rule/rule-page.component'; export * from './pages/rules/rule.component'; export * from './pages/rules/rules-page.component'; export * from './pages/simulator/rule-simulator-page.component'; +export * from './pages/simulator/rule-transition.component'; export * from './pages/simulator/simulated-rule-event.component'; export * from './shared/actions/formattable-input.component'; export * from './shared/actions/generic-action.component'; diff --git a/frontend/app/features/rules/module.ts b/frontend/app/features/rules/module.ts index 11302f01b..5974b764f 100644 --- a/frontend/app/features/rules/module.ts +++ b/frontend/app/features/rules/module.ts @@ -8,7 +8,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HelpComponent, RuleMustExistGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; -import { AssetChangedTriggerComponent, CommentTriggerComponent, ContentChangedTriggerComponent, GenericActionComponent, RuleComponent, RuleElementComponent, RuleEventsPageComponent, RuleIconComponent, RuleSimulatorPageComponent, RulesPageComponent, SchemaChangedTriggerComponent, UsageTriggerComponent } from './declarations'; +import { AssetChangedTriggerComponent, CommentTriggerComponent, ContentChangedTriggerComponent, GenericActionComponent, RuleComponent, RuleElementComponent, RuleEventsPageComponent, RuleIconComponent, RuleSimulatorPageComponent, RulesPageComponent, RuleTransitionComponent, SchemaChangedTriggerComponent, UsageTriggerComponent } from './declarations'; import { RuleEventComponent } from './pages/events/rule-event.component'; import { RulePageComponent } from './pages/rule/rule-page.component'; import { SimulatedRuleEventComponent } from './pages/simulator/simulated-rule-event.component'; @@ -78,6 +78,7 @@ const routes: Routes = [ RuleIconComponent, RulePageComponent, RuleSimulatorPageComponent, + RuleTransitionComponent, RulesPageComponent, SchemaChangedTriggerComponent, SimulatedRuleEventComponent, diff --git a/frontend/app/features/rules/pages/simulator/rule-transition.component.html b/frontend/app/features/rules/pages/simulator/rule-transition.component.html new file mode 100644 index 000000000..2b0d4b546 --- /dev/null +++ b/frontend/app/features/rules/pages/simulator/rule-transition.component.html @@ -0,0 +1,9 @@ +
+ {{ text | sqxTranslate }} +
+ + +
+ {{ error | sqxTranslate }} +
+
\ No newline at end of file diff --git a/frontend/app/features/rules/pages/simulator/rule-transition.component.scss b/frontend/app/features/rules/pages/simulator/rule-transition.component.scss new file mode 100644 index 000000000..c4f8a6c9a --- /dev/null +++ b/frontend/app/features/rules/pages/simulator/rule-transition.component.scss @@ -0,0 +1,32 @@ +.history { + &-transition { + font-size: 85%; + font-weight: normal; + margin-bottom: 1rem; + margin-top: 1rem; + position: relative; + + &::before { + @include circle($history-dot-sm-size); + @include absolute(.5rem, null, null, -1.5rem); + background: $color-border; + content: ''; + margin: 0; + margin-left: $history-dot-sm-offset-x; + } + } +} + +:host { + &:first-child { + .history-transition { + margin-top: 0; + } + } + + &:last-child { + .history-transition { + margin-bottom: 0; + } + } +} \ No newline at end of file diff --git a/frontend/app/features/rules/pages/simulator/rule-transition.component.ts b/frontend/app/features/rules/pages/simulator/rule-transition.component.ts new file mode 100644 index 000000000..b59c88f25 --- /dev/null +++ b/frontend/app/features/rules/pages/simulator/rule-transition.component.ts @@ -0,0 +1,42 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { SimulatedRuleEventDto } from '@app/shared'; + +@Component({ + selector: 'sqx-rule-transition', + styleUrls: ['./rule-transition.component.scss'], + templateUrl: './rule-transition.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RuleTransitionComponent { + @Input() + public event: SimulatedRuleEventDto | undefined | null; + + @Input() + public errors: ReadonlyArray | undefined | null; + + @Input() + public text: string | undefined | null; + + public get filteredErrors() { + const errors = this.errors; + + if (!errors) { + return null; + } + + const result = this.event?.skipReasons.filter(x => errors.indexOf(x) >= 0).map(x => `rules.simulation.error${x}`); + + if (result?.length === 0) { + return null; + } + + return result; + } +} diff --git a/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.html b/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.html index 4ceff3316..5e8c9bec4 100644 --- a/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.html +++ b/frontend/app/features/rules/pages/simulator/simulated-rule-event.component.html @@ -6,7 +6,7 @@ {{event.eventName}} - {{event.skipReason}} + {{event.skipReasons.join(', ')}}