diff --git a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs index 8466de2f9..0fdf3f892 100644 --- a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs @@ -34,9 +34,10 @@ namespace Squidex.Extensions.Actions.AzureQueue protected override async Task<(string Description, AzureQueueJob Data)> CreateJobAsync(EnrichedEvent @event, AzureQueueAction action) { - var requestBody = string.Empty; var queueName = await FormatAsync(action.Queue, @event); + string requestBody; + if (!string.IsNullOrEmpty(action.Payload)) { requestBody = await FormatAsync(action.Payload, @event); diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs index 6be4938bd..d7d941c4d 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs @@ -30,6 +30,7 @@ namespace Squidex.Extensions.Actions.Webhook protected override async Task<(string Description, WebhookJob Data)> CreateJobAsync(EnrichedEvent @event, WebhookAction action) { + var requestUrl = await FormatAsync(action.Url, @event); var requestBody = string.Empty; var requestSignature = string.Empty; @@ -47,8 +48,6 @@ namespace Squidex.Extensions.Actions.Webhook requestSignature = $"{requestBody}{action.SharedSecret}".Sha256Base64(); } - var requestUrl = await FormatAsync(action.Url, @event); - var ruleDescription = $"Send event to webhook '{requestUrl}'"; var ruleJob = new WebhookJob { diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs index 81b317ee0..71b03f164 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs @@ -9,6 +9,8 @@ using System; using System.Runtime.Serialization; using Squidex.Shared.Users; +#pragma warning disable CA1822 // Mark members as static + namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents { public sealed class EnrichedCommentEvent : EnrichedUserEventBase @@ -20,9 +22,20 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents [IgnoreDataMember] public IUser MentionedUser { get; set; } + [IgnoreDataMember] public override long Partition { get { return MentionedUser.Id.GetHashCode(); } } + + public bool ShouldSerializeMentionedUser() + { + return false; + } + + public bool ShouldSerializePartition() + { + return false; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs index 6a57cc350..3acc50943 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs @@ -9,6 +9,8 @@ using System.Runtime.Serialization; using Squidex.Infrastructure; using Squidex.Shared.Users; +#pragma warning disable CA1822 // Mark members as static + namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents { public abstract class EnrichedUserEventBase : EnrichedEvent @@ -17,5 +19,10 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents [IgnoreDataMember] public IUser? User { get; set; } + + public bool ShouldSerializeUser() + { + return false; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs new file mode 100644 index 000000000..1e5db2d49 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventJsonSchemaGenerator.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NJsonSchema; +using NJsonSchema.Generation; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class EventJsonSchemaGenerator + { + private readonly Lazy> schemas; + private readonly JsonSchemaGenerator schemaGenerator; + + public IReadOnlyCollection AllTypes + { + get { return schemas.Value.Keys; } + } + + public EventJsonSchemaGenerator(JsonSchemaGenerator schemaGenerator) + { + Guard.NotNull(schemaGenerator, nameof(schemaGenerator)); + + this.schemaGenerator = schemaGenerator; + + schemas = new Lazy>(GenerateSchemas); + } + + public JsonSchema? GetSchema(string typeName) + { + Guard.NotNull(typeName, nameof(typeName)); + + return schemas.Value.GetOrDefault(typeName); + } + + private Dictionary GenerateSchemas() + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var baseType = typeof(EnrichedEvent); + + var assembly = baseType.Assembly; + + foreach (var type in assembly.GetTypes()) + { + if (!type.IsAbstract && type.IsAssignableTo(baseType)) + { + var schema = schemaGenerator.Generate(type); + + result[type.Name] = schema!; + } + } + + return result; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs index b0beea647..bc86bccec 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/OpenApiServices.cs @@ -5,9 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using NJsonSchema; +using NJsonSchema.Generation; using NJsonSchema.Generation.TypeMappers; using NodaTime; using NSwag.Generation; @@ -52,6 +55,18 @@ namespace Squidex.Areas.Api.Config.OpenApi services.AddSingletonAs() .As(); + services.AddSingleton(c => + { + var settings = ConfigureSchemaSettings(new JsonSchemaGeneratorSettings + { + FlattenInheritanceHierarchy = true, + SerializerOptions = null, + SerializerSettings = c.GetRequiredService() + }); + + return new JsonSchemaGenerator(settings); + }); + services.AddOpenApiDocument(settings => { settings.ConfigureName(); @@ -68,21 +83,26 @@ namespace Squidex.Areas.Api.Config.OpenApi settings.Title = "Squidex API"; } - public static void ConfigureSchemaSettings(this T settings) where T : OpenApiDocumentGeneratorSettings + public static T ConfigureSchemaSettings(this T settings) where T : JsonSchemaGeneratorSettings { settings.AllowReferencesWithProperties = true; settings.TypeMappers = new List { + CreateStringMap(), CreateStringMap(JsonFormatStrings.DateTime), CreateStringMap(), - CreateStringMap(), + CreateStringMap>(), + CreateStringMap>(), + CreateStringMap>(), CreateStringMap(), CreateStringMap(), CreateObjectMap(), CreateObjectMap() }; + + return settings; } private static ITypeMapper CreateObjectMap() diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 0138d4aca..da50ee187 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; @@ -34,19 +35,22 @@ namespace Squidex.Areas.Api.Controllers.Rules private readonly IRuleQueryService ruleQuery; private readonly IRuleRunnerService ruleRunnerService; private readonly IRuleEventRepository ruleEventsRepository; + private readonly EventJsonSchemaGenerator eventJsonSchemaGenerator; private readonly RuleRegistry ruleRegistry; public RulesController(ICommandBus commandBus, IRuleEventRepository ruleEventsRepository, IRuleQueryService ruleQuery, IRuleRunnerService ruleRunnerService, - RuleRegistry ruleRegistry) + RuleRegistry ruleRegistry, + EventJsonSchemaGenerator eventJsonSchemaGenerator) : base(commandBus) { this.ruleEventsRepository = ruleEventsRepository; this.ruleQuery = ruleQuery; this.ruleRunnerService = ruleRunnerService; this.ruleRegistry = ruleRegistry; + this.eventJsonSchemaGenerator = eventJsonSchemaGenerator; } /// @@ -355,6 +359,47 @@ namespace Squidex.Areas.Api.Controllers.Rules return NoContent(); } + /// + /// Provide a list of all event types that are used in rules. + /// + /// + /// 200 => Rule events returned. + /// + [HttpGet] + [Route("rules/eventtypes")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [AllowAnonymous] + public IActionResult GetEventTypes() + { + var types = eventJsonSchemaGenerator.AllTypes; + + return Ok(types); + } + + /// + /// Provide the json schema for the event with the specified name. + /// + /// The name of the event. + /// + /// 200 => Rule event type found. + /// 404 => Rule event not found. + /// + [HttpGet] + [Route("rules/eventtypes/{name}")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [AllowAnonymous] + public IActionResult GetEventSchema(string name) + { + var schema = eventJsonSchemaGenerator.GetSchema(name); + + if (schema == null) + { + return NotFound(); + } + + return Content(schema.ToJson(), "application/json"); + } + private async Task InvokeCommandAsync(ICommand command) { var context = await CommandBus.PublishAsync(command); diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index 9b0571d05..ad88590b3 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -80,6 +80,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().As(); + services.AddSingletonAs() + .AsSelf(); + services.AddSingletonAs() .As().AsSelf(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventJsonSchemaGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventJsonSchemaGeneratorTests.cs new file mode 100644 index 000000000..487998dba --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventJsonSchemaGeneratorTests.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using NJsonSchema.Generation; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.HandleRules +{ + public class EventJsonSchemaGeneratorTests + { + private readonly EventJsonSchemaGenerator sut; + + public EventJsonSchemaGeneratorTests() + { + var jsonSchemaGenerator = + new JsonSchemaGenerator( + new JsonSchemaGeneratorSettings()); + + sut = new EventJsonSchemaGenerator(jsonSchemaGenerator); + } + + public static IEnumerable AllTypes() + { + yield return nameof(EnrichedAssetEvent); + yield return nameof(EnrichedCommentEvent); + yield return nameof(EnrichedContentEvent); + yield return nameof(EnrichedManualEvent); + yield return nameof(EnrichedSchemaEvent); + yield return nameof(EnrichedUsageExceededEvent); + } + + public static IEnumerable AllTypesData() + { + return AllTypes().Select(x => new object[] { x }); + } + + [Fact] + public void Should_return_null_for_unknown_type_name() + { + var schema = sut.GetSchema("Unknown"); + + Assert.Null(schema); + } + + [Fact] + public void Should_provide_all_types() + { + var allTypes = sut.AllTypes; + + Assert.Equal(AllTypes().ToList(), allTypes); + } + + [Theory] + [MemberData(nameof(AllTypesData))] + public void Should_generate_json_schema_for_known_event(string typeName) + { + var schema = sut.GetSchema(typeName); + + Assert.NotNull(schema); + } + } +}