diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs index 5c6dd5b87..c0067d390 100644 --- a/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs +++ b/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs @@ -30,7 +30,7 @@ namespace Squidex.Extensions.Actions.Fastly protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action) { - var id = @event is EnrichedEntityEvent entityEvent ? entityEvent.Id.ToString() : string.Empty; + var id = @event is IEnrichedEntityEvent entityEvent ? entityEvent.Id.ToString() : string.Empty; var ruleJob = new FastlyJob { diff --git a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs index de21e0d0f..dac6d42dc 100644 --- a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs +++ b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure; namespace Squidex.Extensions.Actions @@ -19,40 +18,12 @@ namespace Squidex.Extensions.Actions { private const string ActionSuffix = "Action"; private const string ActionSuffixV2 = "Action"; - private const string TriggerSuffix = "Trigger"; - private const string TriggerSuffixV2 = "TriggerV2"; - private static readonly HashSet ActionHandlerTypes = new HashSet(); - private static readonly Dictionary ActionTypes = new Dictionary(); - private static readonly Dictionary TriggerTypes = new Dictionary - { - [GetTriggerName(typeof(ContentChangedTriggerV2))] = new RuleElement - { - IconImage = "", - IconColor = "#3389ff", - Display = "Content changed", - Description = "For content changes like created, updated, published, unpublished..." - }, - [GetTriggerName(typeof(AssetChangedTriggerV2))] = new RuleElement - { - IconImage = "", - IconColor = "#3389ff", - Display = "Asset changed", - Description = "For asset changes like uploaded, updated (reuploaded), renamed, deleted..." - }, - [GetTriggerName(typeof(UsageTrigger))] = new RuleElement - { - IconImage = "", - IconColor = "#3389ff", - Display = "Usage exceeded", - Description = "When monthly API calls exceed a specified limit for one time a month..." - } - }; public static IReadOnlyDictionary Triggers { - get { return TriggerTypes; } + get { return TriggerTypes.All; } } public static IReadOnlyDictionary Actions @@ -110,10 +81,5 @@ namespace Squidex.Extensions.Actions { return type.TypeName(false, ActionSuffix, ActionSuffixV2); } - - private static string GetTriggerName(Type type) - { - return type.TypeName(false, TriggerSuffix, TriggerSuffixV2); - } } } diff --git a/extensions/Squidex.Extensions/Actions/TriggerTypes.cs b/extensions/Squidex.Extensions/Actions/TriggerTypes.cs new file mode 100644 index 000000000..731a280f9 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/TriggerTypes.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure; + +namespace Squidex.Extensions.Actions +{ + public static class TriggerTypes + { + private const string TriggerSuffix = "Trigger"; + private const string TriggerSuffixV2 = "TriggerV2"; + + public static readonly IReadOnlyDictionary All = new Dictionary + { + [GetTriggerName(typeof(ContentChangedTriggerV2))] = new RuleElement + { + IconImage = "", + IconColor = "#3389ff", + Display = "Content changed", + Description = "For content changes like created, updated, published, unpublished..." + }, + [GetTriggerName(typeof(AssetChangedTriggerV2))] = new RuleElement + { + IconImage = "", + IconColor = "#3389ff", + Display = "Asset changed", + Description = "For asset changes like uploaded, updated (reuploaded), renamed, deleted..." + }, + [GetTriggerName(typeof(SchemaChangedTrigger))] = new RuleElement + { + IconImage = "", + IconColor = "#3389ff", + Display = "Schema changed", + Description = "When a schema definition has been created, updated, published or deleted..." + }, + [GetTriggerName(typeof(UsageTrigger))] = new RuleElement + { + IconImage = "", + IconColor = "#3389ff", + Display = "Usage exceeded", + Description = "When monthly API calls exceed a specified limit for one time a month..." + } + }; + + private static string GetTriggerName(Type type) + { + return type.TypeName(false, TriggerSuffix, TriggerSuffixV2); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs index b3c97d8bd..fca79a600 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs @@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Rules T Visit(ContentChangedTriggerV2 trigger); + T Visit(SchemaChangedTrigger trigger); + T Visit(UsageTrigger trigger); } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs new file mode 100644 index 000000000..bd84236c3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Rules.Triggers +{ + [TypeName(nameof(SchemaChangedTrigger))] + public sealed class SchemaChangedTrigger : RuleTrigger + { + public string Condition { get; set; } + + public override T Accept(IRuleTriggerVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs index c8b4d5a0a..d77deaa9d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedAssetEvent.cs @@ -5,12 +5,26 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using NodaTime; +using Squidex.Infrastructure; + namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { - public sealed class EnrichedAssetEvent : EnrichedEntityEvent + public sealed class EnrichedAssetEvent : EnrichedUserEventBase, IEnrichedEntityEvent { public EnrichedAssetEventType Type { get; set; } + public Guid Id { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + public string MimeType { get; set; } public string FileName { get; set; } @@ -24,5 +38,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents public int? PixelWidth { get; set; } public int? PixelHeight { get; set; } + + public override long Partition + { + get { return Id.GetHashCode(); } + } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs index a381d4c74..70c05f16a 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEvent.cs @@ -5,16 +5,34 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { - public sealed class EnrichedContentEvent : EnrichedSchemaEvent + public sealed class EnrichedContentEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent { public EnrichedContentEventType Type { get; set; } + public Guid Id { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + public NamedContentData Data { get; set; } public Status Status { get; set; } + + public override long Partition + { + get { return SchemaId.GetHashCode(); } + } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs index 824fac4af..82735f4d8 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEvent.cs @@ -6,12 +6,21 @@ // ========================================================================== using System; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { - public abstract class EnrichedSchemaEvent : EnrichedEntityEvent + public sealed class EnrichedSchemaEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent { - public NamedId SchemaId { get; set; } + public EnrichedSchemaEventType Type { get; set; } + + public Guid Id + { + get { return SchemaId.Id; } + } + + public override long Partition + { + get { return SchemaId.GetHashCode(); } + } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs similarity index 53% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs rename to src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs index 1e2d882dc..25b801671 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedEntityEvent.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventBase.cs @@ -6,26 +6,12 @@ // ========================================================================== using System; -using NodaTime; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { - public abstract class EnrichedEntityEvent : EnrichedUserEvent + public abstract class EnrichedSchemaEventBase : EnrichedUserEventBase { - public Guid Id { get; set; } - - public Instant Created { get; set; } - - public Instant LastModified { get; set; } - - public RefToken CreatedBy { get; set; } - - public RefToken LastModifiedBy { get; set; } - - public override long Partition - { - get { return Id.GetHashCode(); } - } + public NamedId SchemaId { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs new file mode 100644 index 000000000..a996998fb --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedSchemaEventType.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents +{ + public enum EnrichedSchemaEventType + { + Created, + Deleted, + Published, + Unpublished, + Updated + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs similarity index 91% rename from src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEvent.cs rename to src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs index 56a011b35..e21540f2d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEvent.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedUserEventBase.cs @@ -11,7 +11,7 @@ using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { - public abstract class EnrichedUserEvent : EnrichedEvent + public abstract class EnrichedUserEventBase : EnrichedEvent { public RefToken Actor { get; set; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs new file mode 100644 index 000000000..a67e82e07 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/IEnrichedEntityEvent.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents +{ + public interface IEnrichedEntityEvent + { + Guid Id { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs index 3597d90c0..f685f1d7e 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules { enrichedEvent.Timestamp = @event.Headers.Timestamp(); - if (enrichedEvent is EnrichedUserEvent userEvent) + if (enrichedEvent is EnrichedUserEventBase userEvent) { if (@event.Payload is SquidexEvent squidexEvent) { diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 647412f70..807210461 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -184,7 +184,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules private static string SchemaId(EnrichedEvent @event) { - if (@event is EnrichedSchemaEvent schemaEvent) + if (@event is EnrichedSchemaEventBase schemaEvent) { return schemaEvent.SchemaId.Id.ToString(); } @@ -194,7 +194,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules private static string SchemaName(EnrichedEvent @event) { - if (@event is EnrichedSchemaEvent schemaEvent) + if (@event is EnrichedSchemaEventBase schemaEvent) { return schemaEvent.SchemaId.Name; } @@ -224,7 +224,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules private static string UserName(EnrichedEvent @event) { - if (@event is EnrichedUserEvent userEvent) + if (@event is EnrichedUserEventBase userEvent) { return userEvent.User?.DisplayName() ?? Fallback; } @@ -234,7 +234,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules private static string UserId(EnrichedEvent @event) { - if (@event is EnrichedUserEvent userEvent) + if (@event is EnrichedUserEventBase userEvent) { return userEvent.User?.Id ?? Fallback; } @@ -244,7 +244,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules private static string UserEmail(EnrichedEvent @event) { - if (@event is EnrichedUserEvent userEvent) + if (@event is EnrichedUserEventBase userEvent) { return userEvent.User?.Email ?? Fallback; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index 835e3125a..dc0251a40 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.Log; namespace Squidex.Domain.Apps.Core.HandleRules { @@ -27,6 +28,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules private readonly IEventEnricher eventEnricher; private readonly IJsonSerializer jsonSerializer; private readonly IClock clock; + private readonly ISemanticLog log; public RuleService( IEnumerable ruleTriggerHandlers, @@ -34,6 +36,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules IEventEnricher eventEnricher, IJsonSerializer jsonSerializer, IClock clock, + ISemanticLog log, TypeNameRegistry typeNameRegistry) { Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); @@ -42,6 +45,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); Guard.NotNull(eventEnricher, nameof(eventEnricher)); Guard.NotNull(clock, nameof(clock)); + Guard.NotNull(log, nameof(log)); this.typeNameRegistry = typeNameRegistry; @@ -53,6 +57,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules this.jsonSerializer = jsonSerializer; this.clock = clock; + this.log = log; } public virtual async Task CreateJobAsync(Rule rule, Guid ruleId, Envelope @event) @@ -60,84 +65,95 @@ namespace Squidex.Domain.Apps.Core.HandleRules Guard.NotNull(rule, nameof(rule)); Guard.NotNull(@event, nameof(@event)); - if (!rule.IsEnabled) + try { - return null; - } + if (!rule.IsEnabled) + { + return null; + } - if (!(@event.Payload is AppEvent)) - { - return null; - } + if (!(@event.Payload is AppEvent)) + { + return null; + } - var typed = @event.To(); + var typed = @event.To(); - var actionType = rule.Action.GetType(); + var actionType = rule.Action.GetType(); - if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) - { - return null; - } + if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) + { + return null; + } - if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) - { - return null; - } + if (!ruleActionHandlers.TryGetValue(actionType, out var actionHandler)) + { + return null; + } - var now = clock.GetCurrentInstant(); + var now = clock.GetCurrentInstant(); - var eventTime = - @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? - @event.Headers.Timestamp() : - now; + var eventTime = + @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? + @event.Headers.Timestamp() : + now; - var expires = eventTime.Plus(Constants.ExpirationTime); + var expires = eventTime.Plus(Constants.ExpirationTime); - if (expires < now) - { - return null; - } + if (expires < now) + { + return null; + } - if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId)) - { - return null; - } + if (!triggerHandler.Trigger(typed.Payload, rule.Trigger, ruleId)) + { + return null; + } - var appEventEnvelope = @event.To(); + var appEventEnvelope = @event.To(); - var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope); + var enrichedEvent = await triggerHandler.CreateEnrichedEventAsync(appEventEnvelope); - if (enrichedEvent == null) - { - return null; - } + if (enrichedEvent == null) + { + return null; + } - await eventEnricher.EnrichAsync(enrichedEvent, typed); + await eventEnricher.EnrichAsync(enrichedEvent, typed); - if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) - { - return null; - } + if (!triggerHandler.Trigger(enrichedEvent, rule.Trigger)) + { + return null; + } - var actionName = typeNameRegistry.GetName(actionType); - var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action); + var actionName = typeNameRegistry.GetName(actionType); + var actionData = await actionHandler.CreateJobAsync(enrichedEvent, rule.Action); - var json = jsonSerializer.Serialize(actionData.Data); + var json = jsonSerializer.Serialize(actionData.Data); - var job = new RuleJob + var job = new RuleJob + { + JobId = Guid.NewGuid(), + ActionName = actionName, + ActionData = json, + AppId = enrichedEvent.AppId.Id, + Created = now, + EventName = enrichedEvent.Name, + ExecutionPartition = enrichedEvent.Partition, + Expires = expires, + Description = actionData.Description + }; + + return job; + } + catch (Exception ex) { - JobId = Guid.NewGuid(), - ActionName = actionName, - ActionData = json, - AppId = enrichedEvent.AppId.Id, - Created = now, - EventName = enrichedEvent.Name, - ExecutionPartition = enrichedEvent.Partition, - Expires = expires, - Description = actionData.Description - }; - - return job; + log.LogError(ex, w => w + .WriteProperty("action", "createRuleJob") + .WriteProperty("status", "Failed")); + + return null; + } } public virtual async Task<(string Dump, RuleResult Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 60726b883..dff657548 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -132,7 +132,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return eventId.Id == schema.SchemaId; } - private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEvent @event) + private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) { return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index 3307b3b71..0465ae1fe 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -40,6 +40,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards return Task.FromResult(Enumerable.Empty()); } + public Task> Visit(SchemaChangedTrigger trigger) + { + return Task.FromResult(Enumerable.Empty()); + } + public Task> Visit(UsageTrigger trigger) { var errors = new List(); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs new file mode 100644 index 000000000..46dd6c0e7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public sealed class SchemaChangedTriggerHandler : RuleTriggerHandler + { + private readonly IScriptEngine scriptEngine; + + public SchemaChangedTriggerHandler(IScriptEngine scriptEngine) + { + Guard.NotNull(scriptEngine, nameof(scriptEngine)); + + this.scriptEngine = scriptEngine; + } + + protected override Task CreateEnrichedEventAsync(Envelope @event) + { + var result = new EnrichedSchemaEvent(); + + SimpleMapper.Map(@event.Payload, result); + + switch (@event.Payload) + { + case FieldEvent _: + case SchemaPreviewUrlsConfigured _: + case SchemaScriptsConfigured _: + case SchemaUpdated _: + case ParentFieldEvent _: + result.Type = EnrichedSchemaEventType.Updated; + break; + case SchemaCreated _: + result.Type = EnrichedSchemaEventType.Created; + break; + case SchemaPublished _: + result.Type = EnrichedSchemaEventType.Published; + break; + case SchemaUnpublished _: + result.Type = EnrichedSchemaEventType.Unpublished; + break; + case SchemaDeleted _: + result.Type = EnrichedSchemaEventType.Deleted; + break; + default: + result = null; + break; + } + + result.Name = $"Schema{result.Type}"; + + return Task.FromResult(result); + } + + protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger) + { + return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs index 25fb14496..2b9bd6328 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -31,6 +31,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto()); } + public RuleTriggerDto Visit(SchemaChangedTrigger trigger) + { + return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto()); + } + public RuleTriggerDto Visit(UsageTrigger trigger) { return SimpleMapper.Map(trigger, new UsageRuleTriggerDto()); diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs new file mode 100644 index 000000000..c94475af7 --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/SchemaChangedRuleTriggerDto.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers +{ + public sealed class SchemaChangedRuleTriggerDto : RuleTriggerDto + { + /// + /// Javascript condition when to trigger. + /// + public string Condition { get; set; } + + public override RuleTrigger ToTrigger() + { + return SimpleMapper.Map(this, new SchemaChangedTrigger()); + } + } +} diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs index f178db4de..02f36ecc3 100644 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ b/src/Squidex/Config/Domain/RuleServices.cs @@ -11,6 +11,7 @@ using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Extensions.Actions; using Squidex.Infrastructure.EventSourcing; @@ -29,6 +30,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts index eb57c8359..6b65eccfb 100644 --- a/src/Squidex/app/features/rules/declarations.ts +++ b/src/Squidex/app/features/rules/declarations.ts @@ -19,6 +19,7 @@ export * from './pages/rules/actions/webhook-action.component'; export * from './pages/rules/triggers/asset-changed-trigger.component'; export * from './pages/rules/triggers/content-changed-trigger.component'; +export * from './pages/rules/triggers/schema-changed-trigger.component'; export * from './pages/rules/triggers/usage-trigger.component'; export * from './pages/rules/rule-element.component'; diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index 55d204251..95551399c 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -30,6 +30,7 @@ import { RuleEventsPageComponent, RulesPageComponent, RuleWizardComponent, + SchemaChangedTriggerComponent, SlackActionComponent, TweetActionComponent, UsageTriggerComponent, @@ -78,6 +79,7 @@ const routes: Routes = [ RuleEventsPageComponent, RulesPageComponent, RuleWizardComponent, + SchemaChangedTriggerComponent, SlackActionComponent, TweetActionComponent, UsageTriggerComponent, diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html index 54a41851e..16f1fca8e 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -49,6 +49,13 @@ [triggerFormSubmitted]="triggerForm.submitted | async"> + + + + +
+ + + + + +
+ +
+

Conditions

+ +

Conditions are javascript expressions that define when to trigger, for example:

+ +
    +
  • + Specific events:
    + + event.type == 'Created' || event.type == 'Updated' +
  • +
  • + Specific schema only:
    + + schemaId.name === 'my-schema' +
  • +
+
+ \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss b/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss new file mode 100644 index 000000000..6e1eef5ec --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.scss @@ -0,0 +1,6 @@ +@import '_vars'; +@import '_mixins'; + +textarea { + height: 100px; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts new file mode 100644 index 000000000..4096a8410 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts @@ -0,0 +1,30 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'sqx-schema-changed-trigger', + styleUrls: ['./schema-changed-trigger.component.scss'], + templateUrl: './schema-changed-trigger.component.html' +}) +export class SchemaChangedTriggerComponent implements OnInit { + @Input() + public trigger: any; + + @Input() + public triggerForm: FormGroup; + + @Input() + public triggerFormSubmitted = false; + + public ngOnInit() { + this.triggerForm.setControl('condition', + new FormControl(this.trigger.condition || '')); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts index ab63bab3f..1cd5fd4c1 100644 --- a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, Input, OnInit, Renderer2 } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { timer } from 'rxjs'; import { ResourceOwner } from '@app/framework/internal'; @@ -23,7 +23,7 @@ const POSITION_FULL = 'full'; @Directive({ selector: '[sqxModalTarget]' }) -export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnInit { +export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy, OnInit { private targetElement: any; @Input('sqxModalTarget') diff --git a/src/Squidex/app/framework/angular/modals/tooltip.component.ts b/src/Squidex/app/framework/angular/modals/tooltip.component.ts index 8d25f819e..5466ceb23 100644 --- a/src/Squidex/app/framework/angular/modals/tooltip.component.ts +++ b/src/Squidex/app/framework/angular/modals/tooltip.component.ts @@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, R import { fadeAnimation, ModalModel, - StatefulComponent + ResourceOwner } from '@app/framework/internal'; @Component({ @@ -22,7 +22,7 @@ import { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TooltipComponent extends StatefulComponent implements OnInit { +export class TooltipComponent extends ResourceOwner implements OnInit { @Input() public target: any; @@ -31,10 +31,11 @@ export class TooltipComponent extends StatefulComponent implements OnInit { public modal = new ModalModel(); - constructor(changeDetector: ChangeDetectorRef, + constructor( + private readonly changeDetector: ChangeDetectorRef, private readonly renderer: Renderer2 ) { - super(changeDetector, {}); + super(); } public ngOnInit() { @@ -42,6 +43,8 @@ export class TooltipComponent extends StatefulComponent implements OnInit { this.own( this.renderer.listen(this.target, 'mouseenter', () => { this.modal.show(); + + this.changeDetector.detectChanges(); })); this.own( diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 8a8f266ab..99bbf62ca 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; using Xunit; #pragma warning disable xUnit2009 // Do not use boolean check to check for string equality @@ -80,7 +81,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.TriggerType) .Returns(typeof(ContentChangedTriggerV2)); - sut = new RuleService(new[] { ruleTriggerHandler }, new[] { ruleActionHandler }, eventEnricher, TestUtils.DefaultSerializer, clock, typeNameRegistry); + var log = A.Fake(); + + sut = new RuleService(new[] { ruleTriggerHandler }, new[] { ruleActionHandler }, eventEnricher, TestUtils.DefaultSerializer, clock, log, typeNameRegistry); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs new file mode 100644 index 000000000..43cb7bf55 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public class SchemaChangedTriggerHandlerTests + { + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IRuleTriggerHandler sut; + + public SchemaChangedTriggerHandlerTests() + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) + .Returns(true); + + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) + .Returns(false); + + sut = new SchemaChangedTriggerHandler(scriptEngine); + } + + public static IEnumerable TestEvents = new[] + { + new object[] { new SchemaCreated(), EnrichedSchemaEventType.Created }, + new object[] { new SchemaUpdated(), EnrichedSchemaEventType.Updated }, + new object[] { new SchemaDeleted(), EnrichedSchemaEventType.Deleted }, + new object[] { new SchemaPublished(), EnrichedSchemaEventType.Published }, + new object[] { new SchemaUnpublished(), EnrichedSchemaEventType.Unpublished } + }; + + [Theory] + [MemberData(nameof(TestEvents))] + public async Task Should_enrich_events(SchemaEvent @event, EnrichedSchemaEventType type) + { + var envelope = Envelope.Create(@event).SetEventStreamNumber(12); + + var schemaGrain = A.Fake(); + + var result = await sut.CreateEnrichedEventAsync(envelope); + + Assert.Equal(type, ((EnrichedSchemaEvent)result).Type); + } + + [Fact] + public void Should_not_trigger_precheck_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new AppCreated(), trigger, Guid.NewGuid()); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_precheck_when_event_type_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new SchemaCreated(), trigger, Guid.NewGuid()); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_event_type_not_correct() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedContentEvent(), trigger); + + Assert.False(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_is_empty() + { + TestForCondition(string.Empty, trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_trigger_check_when_condition_matchs() + { + TestForCondition("true", trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.True(result); + }); + } + + [Fact] + public void Should_not_trigger_check_when_condition_does_not_matchs() + { + TestForCondition("false", trigger => + { + var result = sut.Trigger(new EnrichedSchemaEvent(), trigger); + + Assert.False(result); + }); + } + + private void TestForCondition(string condition, Action action) + { + var trigger = new SchemaChangedTrigger { Condition = condition }; + + action(trigger); + + if (string.IsNullOrWhiteSpace(condition)) + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustNotHaveHappened(); + } + else + { + A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, condition)) + .MustHaveHappened(); + } + } + } +}