From 7c3a6dbb3c6e13d07cdbe61b0fb0c6bcd8caf4f4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 30 Sep 2020 12:08:18 +0200 Subject: [PATCH] Create content action. (#584) --- .../Actions/Comment/CommentActionHandler.cs | 46 ++++------ .../CreateContent/CreateContentAction.cs | 42 +++++++++ .../CreateContentActionHandler.cs | 87 +++++++++++++++++++ .../CreateContent/CreateContentPlugin.cs | 21 +++++ .../Notification/NotificationActionHandler.cs | 27 ++---- .../Contents/ContentFieldData.cs | 6 ++ .../Contents/NamedContentData.cs | 6 ++ .../HandleRules/RuleService.cs | 5 ++ .../ContentWrapper/ContentDataObject.cs | 7 ++ .../ContentWrapper/ContentFieldObject.cs | 6 ++ .../SquidexCommand.cs | 2 + .../SquidexEvent.cs | 2 + .../RuleEventFormatterCompareTests.cs | 23 +++++ .../HandleRules/RuleServiceTests.cs | 18 ++++ .../actions/generic-action.component.html | 4 +- 15 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs create mode 100644 backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs create mode 100644 backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs index c2b89fb54..cc3141a46 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.HandleRules; @@ -13,11 +12,10 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Reflection; namespace Squidex.Extensions.Actions.Comment { - public sealed class CommentActionHandler : RuleActionHandler + public sealed class CommentActionHandler : RuleActionHandler { private const string Description = "Send a Comment"; private readonly ICommandBus commandBus; @@ -30,56 +28,48 @@ namespace Squidex.Extensions.Actions.Comment this.commandBus = commandBus; } - protected override async Task<(string Description, CommentJob Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action) + protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action) { if (@event is EnrichedContentEvent contentEvent) { - var text = await FormatAsync(action.Text, @event); + var ruleJob = new CreateComment + { + AppId = contentEvent.AppId, + }; - var actor = contentEvent.Actor; + ruleJob.Text = await FormatAsync(action.Text, @event); if (!string.IsNullOrEmpty(action.Client)) { - actor = new RefToken(RefTokenType.Client, action.Client); + ruleJob.Actor = new RefToken(RefTokenType.Client, action.Client); } - - var ruleJob = new CommentJob + else { - AppId = contentEvent.AppId, - Actor = actor, - CommentsId = contentEvent.Id.ToString(), - Text = text - }; + ruleJob.Actor = contentEvent.Actor; + } + + ruleJob.CommentsId = contentEvent.Id.ToString(); return (Description, ruleJob); } - return ("Ignore", new CommentJob()); + return ("Ignore", new CreateComment()); } - protected override async Task ExecuteJobAsync(CommentJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(CreateComment job, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(job.CommentsId)) { return Result.Ignored(); } - var command = SimpleMapper.Map(job, new CreateComment()); + var command = job; + + command.FromRule = true; await commandBus.PublishAsync(command); return Result.Success($"Commented: {job.Text}"); } } - - public sealed class CommentJob - { - public NamedId AppId { get; set; } - - public RefToken Actor { get; set; } - - public string CommentsId { get; set; } - - public string Text { get; set; } - } } diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs new file mode 100644 index 000000000..2689a30aa --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Extensions.Actions.CreateContent +{ + [RuleAction( + Title = "CreateContent", + IconImage = "", + IconColor = "#3389ff", + Display = "Create content", + Description = "Create a a new content item for any schema.")] + public sealed class CreateContentAction : RuleAction + { + [LocalizedRequired] + [Display(Name = "Data", Description = "The content data.")] + [DataType(DataType.MultilineText)] + [Formattable] + public string Data { get; set; } + + [LocalizedRequired] + [Display(Name = "Schema", Description = "The name of the schema.")] + [DataType(DataType.Text)] + public string Schema { get; set; } + + [Display(Name = "Client", Description = "An optional client name.")] + [DataType(DataType.Text)] + public string Client { get; set; } + + [Display(Name = "Publish", Description = "Publish the content.")] + [DataType(DataType.Text)] + public bool Publish { get; set; } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs new file mode 100644 index 000000000..3e87610d1 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs @@ -0,0 +1,87 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Json; +using Command = Squidex.Domain.Apps.Entities.Contents.Commands.CreateContent; + +namespace Squidex.Extensions.Actions.CreateContent +{ + public sealed class CreateContentActionHandler : RuleActionHandler + { + private const string Description = "Create a content"; + private readonly ICommandBus commandBus; + private readonly IAppProvider appProvider; + private readonly IJsonSerializer jsonSerializer; + + public CreateContentActionHandler(RuleEventFormatter formatter, IAppProvider appProvider, ICommandBus commandBus, IJsonSerializer jsonSerializer) + : base(formatter) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(commandBus, nameof(commandBus)); + Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); + + this.appProvider = appProvider; + this.commandBus = commandBus; + this.jsonSerializer = jsonSerializer; + } + + protected override async Task<(string Description, Command Data)> CreateJobAsync(EnrichedEvent @event, CreateContentAction action) + { + var ruleJob = new Command + { + AppId = @event.AppId, + }; + + var schema = await appProvider.GetSchemaAsync(@event.AppId.Id, action.Schema, true); + + if (schema == null) + { + throw new InvalidOperationException($"Cannot find schema '{action.Schema}'"); + } + + ruleJob.SchemaId = schema.NamedId(); + + var json = await FormatAsync(action.Data, @event); + + ruleJob.Data = jsonSerializer.Deserialize(json); + + if (!string.IsNullOrEmpty(action.Client)) + { + ruleJob.Actor = new RefToken(RefTokenType.Client, action.Client); + } + else if (@event is EnrichedUserEventBase userEvent) + { + ruleJob.Actor = userEvent.Actor; + } + + ruleJob.Publish = action.Publish; + + return (Description, ruleJob); + } + + protected override async Task ExecuteJobAsync(Command job, CancellationToken ct = default) + { + var command = job; + + command.FromRule = true; + + await commandBus.PublishAsync(command); + + return Result.Success($"Created to: {job.SchemaId.Name}"); + } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs new file mode 100644 index 000000000..644917108 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Plugins; + +namespace Squidex.Extensions.Actions.CreateContent +{ + public sealed class CreateContentPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs index 0093b2335..f1f3f7655 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs @@ -13,12 +13,11 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Reflection; using Squidex.Shared.Users; namespace Squidex.Extensions.Actions.Notification { - public sealed class NotificationActionHandler : RuleActionHandler + public sealed class NotificationActionHandler : RuleActionHandler { private const string Description = "Send a Notification"; private static readonly NamedId NoApp = NamedId.Of(Guid.Empty, "none"); @@ -36,7 +35,7 @@ namespace Squidex.Extensions.Actions.Notification this.userResolver = userResolver; } - protected override async Task<(string Description, NotificationJob Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action) + protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action) { if (@event is EnrichedUserEventBase userEvent) { @@ -56,7 +55,7 @@ namespace Squidex.Extensions.Actions.Notification throw new InvalidOperationException($"Cannot find user by '{action.User}'"); } - var ruleJob = new NotificationJob { Actor = actor, CommentsId = user.Id, Text = text }; + var ruleJob = new CreateComment { Actor = actor, CommentsId = user.Id, Text = text }; if (!string.IsNullOrWhiteSpace(action.Url)) { @@ -71,32 +70,24 @@ namespace Squidex.Extensions.Actions.Notification return (Description, ruleJob); } - return ("Ignore", new NotificationJob()); + return ("Ignore", new CreateComment()); } - protected override async Task ExecuteJobAsync(NotificationJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(CreateComment job, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(job.CommentsId)) { return Result.Ignored(); } - var command = SimpleMapper.Map(job, new CreateComment { AppId = NoApp }); + var command = job; + + command.AppId = NoApp; + command.FromRule = true; await commandBus.PublishAsync(command); return Result.Success($"Notified: {job.Text}"); } } - - public sealed class NotificationJob - { - public RefToken Actor { get; set; } - - public string CommentsId { get; set; } - - public string Text { get; set; } - - public Uri Url { get; set; } - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs index 656e81abe..e0408b1df 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; @@ -88,5 +89,10 @@ namespace Squidex.Domain.Apps.Core.Contents { return this.DictionaryHashCode(); } + + public override string ToString() + { + return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}"; + } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs index 84ed62235..9eec25ad2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs @@ -7,6 +7,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Contents @@ -68,5 +69,10 @@ namespace Squidex.Domain.Apps.Core.Contents { return base.Equals(other); } + + public override string ToString() + { + return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value}"))}}}"; + } } } 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 f35b4feb9..1aeb74cd8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -89,6 +89,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules var typed = @event.To(); + if (typed.Payload.FromRule) + { + return result; + } + var actionType = rule.Action.GetType(); if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs index f57eaca91..813cc2c1a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using Jint; @@ -12,6 +13,7 @@ using Jint.Native; using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Descriptors; +using Orleans; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; @@ -116,6 +118,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper var propertyName = property.AsString(); + if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase)) + { + return PropertyDescriptor.Undefined; + } + return fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false))); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs index f69a49fd9..351a73d61 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using Jint; @@ -131,6 +132,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper var propertyName = property.AsString(); + if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase)) + { + return PropertyDescriptor.Undefined; + } + return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs index e5dbccb20..c1c4d484d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities public ClaimsPrincipal User { get; set; } + public bool FromRule { get; set; } + public long ExpectedVersion { get; set; } = EtagVersion.Auto; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs index 8d42986bf..93cc78a14 100644 --- a/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs @@ -13,5 +13,7 @@ namespace Squidex.Domain.Apps.Events public abstract class SquidexEvent : IEvent { public RefToken Actor { get; set; } + + public bool FromRule { get; set; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs index 2ad6092c7..6afa79c12 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs @@ -705,6 +705,29 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty)); } + [Theory] + [Expressions( + "$CONTENT_DATA", + "${CONTENT_DATA}", + "${JSON.stringify(event.data)}", + null + )] + public async Task Should_return_json_string_when_data(string script) + { + var @event = new EnrichedContentEvent + { + Data = + new NamedContentData() + .AddField("city", + new ContentFieldData() + .AddJsonValue(JsonValue.Object().Add("name", "Berlin"))) + }; + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("{\"city\":{\"iv\":{\"name\":\"Berlin\"}}}", result); + } + [Theory] [Expressions( null, 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 95ac00416..9bf2e961c 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 @@ -173,6 +173,24 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules .MustHaveHappened(); } + [Fact] + public async Task Should_not_create_job_if_event_created_by_rule() + { + var rule = ValidRule(); + + var @event = Envelope.Create(new ContentCreated { FromRule = true }); + + var jobs = await sut.CreateJobsAsync(rule, ruleId, @event); + + Assert.Empty(jobs); + + A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId)) + .MustNotHaveHappened(); + + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A>._)) + .MustNotHaveHappened(); + } + [Fact] public async Task Should_not_create_job_if_not_triggered_with_precheck() { diff --git a/frontend/app/features/rules/pages/rules/actions/generic-action.component.html b/frontend/app/features/rules/pages/rules/actions/generic-action.component.html index 038c5d184..a3fca239d 100644 --- a/frontend/app/features/rules/pages/rules/actions/generic-action.component.html +++ b/frontend/app/features/rules/pages/rules/actions/generic-action.component.html @@ -13,8 +13,8 @@
- -