diff --git a/Dockerfile b/Dockerfile index 6eefd8277..130de2b25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,11 +20,11 @@ RUN cp -a /tmp/node_modules src/Squidex/ \ # Test Backend RUN dotnet restore \ - && dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ + && dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \ - && dotnet test tests/Squidex.Tests/Squidex.Tests.csproj + && dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj # Publish RUN dotnet publish src/Squidex/Squidex.csproj --output /out/alpine --configuration Release -r alpine.3.7-x64 diff --git a/Dockerfile.build b/Dockerfile.build index 6f877d600..30a5d3091 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -17,11 +17,11 @@ RUN cp -a /tmp/node_modules src/Squidex/ \ # Test Backend RUN dotnet restore \ - && dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ + && dotnet test --filter Category!=Dependencies tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \ && dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \ - && dotnet test tests/Squidex.Tests/Squidex.Tests.csproj + && dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj # Publish RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release \ No newline at end of file diff --git a/Squidex.sln b/Squidex.sln index 6d8cfb71d..26a5f4eeb 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -30,10 +30,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_00", "tools\Migrate EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Shared", "src\Squidex.Shared\Squidex.Shared.csproj", "{5E75AB7D-6F01-4313-AFF1-7F7128FFD71F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apps", "apps", "{C9809D59-6665-471E-AD87-5AC624C65892}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "users", "users", "{C0D540F0-9158-4528-BFD8-BEAE6EAE45EA}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users", "src\Squidex.Domain.Users\Squidex.Domain.Users.csproj", "{F7771E22-47BD-45C4-A133-FD7F1DE27CA0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Users.MongoDb", "src\Squidex.Domain.Users.MongoDb\Squidex.Domain.Users.MongoDb.csproj", "{27CF800D-890F-4882-BF05-44EC3233537D}" @@ -61,12 +57,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entitie EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate_01\Migrate_01.csproj", "{A4823E14-C0E5-4A4D-B28F-27424C25C3C7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Tests", "tests\Squidex.Tests\Squidex.Tests.csproj", "{7E8CC864-4C6E-496F-A672-9F9AD8874835}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Web.Tests", "tests\Squidex.Web.Tests\Squidex.Web.Tests.csproj", "{7E8CC864-4C6E-496F-A672-9F9AD8874835}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "extensions", "extensions", "{FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Extensions", "extensions\Squidex.Extensions\Squidex.Extensions.csproj", "{F3C41B82-6A67-409A-B7FE-54543EE4F38B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{7EDE8CF1-B1E4-4005-B154-834B944E0D7A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Web", "src\Squidex.Web\Squidex.Web.csproj", "{5B2D251F-46E3-486A-AE16-E3FE06B559ED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -337,35 +337,47 @@ Global {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x64.Build.0 = Release|Any CPU {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x86.ActiveCfg = Release|Any CPU {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x86.Build.0 = Release|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|x64.Build.0 = Debug|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Debug|x86.Build.0 = Debug|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|Any CPU.Build.0 = Release|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x64.ActiveCfg = Release|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x64.Build.0 = Release|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x86.ActiveCfg = Release|Any CPU + {5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} - {25F66C64-058A-4D44-BC0C-F12A054F9A91} = {C9809D59-6665-471E-AD87-5AC624C65892} + {25F66C64-058A-4D44-BC0C-F12A054F9A91} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} {7FD0A92B-7862-4BB1-932B-B52A9CACB56B} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} - {FD0AFD44-7A93-4F9E-B5ED-72582392E435} = {C9809D59-6665-471E-AD87-5AC624C65892} + {FD0AFD44-7A93-4F9E-B5ED-72582392E435} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} {6A811927-3C37-430A-90F4-503E37123956} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {D7166C56-178A-4457-B56A-C615C7450DEE} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {945871B1-77B8-43FB-B53C-27CF385AB756} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {B51126A8-0D75-4A79-867D-10724EC6AC84} = {94207AA6-4923-4183-A558-E0F8196B8CA3} {5E75AB7D-6F01-4313-AFF1-7F7128FFD71F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} - {C9809D59-6665-471E-AD87-5AC624C65892} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} - {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} - {F7771E22-47BD-45C4-A133-FD7F1DE27CA0} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA} - {27CF800D-890F-4882-BF05-44EC3233537D} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA} - {42184546-E3CB-4D4F-9495-43979B9C63B9} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA} + {F7771E22-47BD-45C4-A133-FD7F1DE27CA0} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A} + {27CF800D-890F-4882-BF05-44EC3233537D} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A} + {42184546-E3CB-4D4F-9495-43979B9C63B9} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A} {EF75E488-1324-4E18-A1BD-D3A05AE67B1F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {7931187E-A1E6-4F89-8BC8-20A1E445579F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} - {F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892} - {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892} - {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {C9809D59-6665-471E-AD87-5AC624C65892} - {AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892} - {7DA5B308-D950-4496-93D5-21D6C4D91644} = {C9809D59-6665-471E-AD87-5AC624C65892} + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} + {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} + {AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} + {7DA5B308-D950-4496-93D5-21D6C4D91644} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} {A4823E14-C0E5-4A4D-B28F-27424C25C3C7} = {94207AA6-4923-4183-A558-E0F8196B8CA3} + {7E8CC864-4C6E-496F-A672-9F9AD8874835} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A} {F3C41B82-6A67-409A-B7FE-54543EE4F38B} = {FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52} + {5B2D251F-46E3-486A-AE16-E3FE06B559ED} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs index baff5dc43..eb6c041a4 100644 --- a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs +++ b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs @@ -6,11 +6,11 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Extensions.Actions.Algolia { - [RuleActionHandler(typeof(AlgoliaActionHandler))] [RuleAction( IconImage = "", IconColor = "#0d9bf9", @@ -21,14 +21,20 @@ namespace Squidex.Extensions.Actions.Algolia { [Required] [Display(Name = "Application Id", Description = "The application ID.")] + [DataType(DataType.Text)] + [Formattable] public string AppId { get; set; } [Required] [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] + [DataType(DataType.Text)] + [Formattable] public string ApiKey { get; set; } [Required] - [Display(Name = "Index Name", Description = "THe name of the index.")] + [Display(Name = "Index Name", Description = "The name of the index.")] + [DataType(DataType.Text)] + [Formattable] public string IndexName { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs new file mode 100644 index 000000000..c668ba003 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.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.Algolia +{ + public sealed class AlgoliaPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs index 0ee5de663..002da0d83 100644 --- a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs +++ b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs @@ -8,12 +8,12 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Extensions.Actions.AzureQueue { - [RuleActionHandler(typeof(AzureQueueActionHandler))] [RuleAction( IconImage = "", IconColor = "#0d9bf9", @@ -23,11 +23,15 @@ namespace Squidex.Extensions.Actions.AzureQueue public sealed class AzureQueueAction : RuleAction { [Required] - [Display(Name = "Connection String", Description = "The connection string to the storage account.")] + [Display(Name = "Connection", Description = "The connection string to the storage account.")] + [DataType(DataType.Text)] + [Formattable] public string ConnectionString { get; set; } [Required] [Display(Name = "Queue", Description = "The name of the queue.")] + [DataType(DataType.Text)] + [Formattable] public string Queue { get; set; } protected override IEnumerable CustomValidate() diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs new file mode 100644 index 000000000..4dce57e2b --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.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.AzureQueue +{ + public sealed class AzureQueuePlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs index 4711dec26..8e1c04011 100644 --- a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs +++ b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs @@ -7,12 +7,12 @@ using System; using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Extensions.Actions.Discourse { - [RuleActionHandler(typeof(DiscourseActionHandler))] [RuleAction( IconImage = "", IconColor = "#eB6121", @@ -23,28 +23,37 @@ namespace Squidex.Extensions.Actions.Discourse { [AbsoluteUrl] [Required] - [Display(Name = "Url", Description = "he url to the discourse server.")] + [Display(Name = "Server Url", Description = "The url to the discourse server.")] + [DataType(DataType.Url)] public Uri Url { get; set; } [Required] [Display(Name = "Api Key", Description = "The api key to authenticate to your discourse server.")] + [DataType(DataType.Text)] public string ApiKey { get; set; } [Required] - [Display(Name = "Api Username", Description = "The api username to authenticate to your discourse server.")] + [Display(Name = "Api User", Description = "The api username to authenticate to your discourse server.")] + [DataType(DataType.Text)] public string ApiUsername { get; set; } [Required] [Display(Name = "Text", Description = "The text as markdown.")] + [DataType(DataType.MultilineText)] + [Formattable] public string Text { get; set; } [Display(Name = "Title", Description = "The optional title when creating new topics.")] + [DataType(DataType.Text)] + [Formattable] public string Title { get; set; } [Display(Name = "Topic", Description = "The optional topic id.")] + [DataType(DataType.Custom)] public int? Topic { get; set; } [Display(Name = "Category", Description = "The optional category id.")] + [DataType(DataType.Custom)] public int? Category { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs index b0811ba17..d408569df 100644 --- a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs +++ b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs @@ -34,7 +34,6 @@ namespace Squidex.Extensions.Actions.Discourse var json = new Dictionary { - ["raw"] = Format(action.Text, @event), ["title"] = Format(action.Title, @event) }; @@ -56,6 +55,8 @@ namespace Squidex.Extensions.Actions.Discourse RequestBody = requestBody }; + json["raw"] = Format(action.Text, @event); + var description = action.Topic.HasValue ? DescriptionCreateTopic : diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs b/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs new file mode 100644 index 000000000..e78e04d54 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.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.Discourse +{ + public sealed class DiscoursePlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs index 620f9ef6e..826f86f6d 100644 --- a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs +++ b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs @@ -7,12 +7,12 @@ using System; using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Extensions.Actions.ElasticSearch { - [RuleActionHandler(typeof(ElasticSearchActionHandler))] [RuleAction( IconImage = "", IconColor = "#1e5470", @@ -23,21 +23,28 @@ namespace Squidex.Extensions.Actions.ElasticSearch { [AbsoluteUrl] [Required] - [Display(Name = "Host", Description = "The hostname of the elastic search instance or cluster.")] + [Display(Name = "Server Url", Description = "The url to the elastic search instance or cluster.")] + [DataType(DataType.Url)] public Uri Host { get; set; } [Required] [Display(Name = "Index Name", Description = "The name of the index.")] + [DataType(DataType.Text)] + [Formattable] public string IndexName { get; set; } [Required] [Display(Name = "Index Type", Description = "The name of the index type.")] + [DataType(DataType.Text)] + [Formattable] public string IndexType { get; set; } [Display(Name = "Username", Description = "The optional username.")] + [DataType(DataType.Text)] public string Username { get; set; } [Display(Name = "Password", Description = "The optional password.")] + [DataType(DataType.Text)] public string Password { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs index 991725964..10c3b0a79 100644 --- a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs +++ b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs @@ -45,9 +45,9 @@ namespace Squidex.Extensions.Actions.ElasticSearch var ruleJob = new ElasticSearchJob { Host = action.Host.ToString(), - ContentId = contentId, IndexName = Format(action.IndexName, @event), - IndexType = Format(action.IndexType, @event) + IndexType = Format(action.IndexType, @event), + ContentId = contentId }; if (contentEvent.Type == EnrichedContentEventType.Deleted || diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs new file mode 100644 index 000000000..a40f610b0 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.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.ElasticSearch +{ + public sealed class ElasticSearchPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs b/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs index 0de1cb24f..e6ab737b7 100644 --- a/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs +++ b/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs @@ -6,11 +6,11 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Extensions.Actions.Email { - [RuleActionHandler(typeof(EmailActionHandler))] [RuleAction( IconImage = "", IconColor = "#333300", @@ -20,39 +20,53 @@ namespace Squidex.Extensions.Actions.Email public sealed class EmailAction : RuleAction { [Required] - [Display(Name = "ServerHost", Description = "The IP address or host to the SMTP server.")] + [Display(Name = "Server Host", Description = "The IP address or host to the SMTP server.")] + [DataType(DataType.Text)] public string ServerHost { get; set; } [Required] - [Display(Name = "ServerPort", Description = "The port to the SMTP server.")] + [Display(Name = "Server Port", Description = "The port to the SMTP server.")] + [DataType(DataType.Custom)] public int ServerPort { get; set; } [Required] - [Display(Name = "ServerUseSsl", Description = "Specify whether the SMPT client uses Secure Sockets Layer (SSL) to encrypt the connection.")] + [Display(Name = "Use SSL", Description = "Specify whether the SMPT client uses Secure Sockets Layer (SSL) to encrypt the connection.")] + [DataType(DataType.Custom)] public bool ServerUseSsl { get; set; } [Required] - [Display(Name = "ServerUsername", Description = "The username for the SMTP server.")] - public string ServerUsername { get; set; } + [Display(Name = "Password", Description = "The password for the SMTP server.")] + [DataType(DataType.Password)] + public string ServerPassword { get; set; } [Required] - [Display(Name = "ServerPassword", Description = "The password for the SMTP server.")] - public string ServerPassword { get; set; } + [Display(Name = "Username", Description = "The username for the SMTP server.")] + [DataType(DataType.Text)] + [Formattable] + public string ServerUsername { get; set; } [Required] - [Display(Name = "MessageFrom", Description = "The email sending address.")] + [Display(Name = "From Address", Description = "The email sending address.")] + [DataType(DataType.Text)] + [Formattable] public string MessageFrom { get; set; } [Required] - [Display(Name = "MessageTo", Description = "The email message will be sent to.")] + [Display(Name = "To Address", Description = "The email message will be sent to.")] + [DataType(DataType.Text)] + [Formattable] public string MessageTo { get; set; } [Required] - [Display(Name = "MessageSubject", Description = "The subject line for this email message.")] + [Display(Name = "Subject", Description = "The subject line for this email message.")] + [DataType(DataType.Text)] + [Formattable] public string MessageSubject { get; set; } [Required] - [Display(Name = "MessageBody", Description = "The message body.")] + [Display(Name = "Body", Description = "The message body.")] + [DataType(DataType.MultilineText)] + [Formattable] public string MessageBody { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs b/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs new file mode 100644 index 000000000..dd9140866 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.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.Email +{ + public sealed class EmailPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs index 928dab189..d227e8ce1 100644 --- a/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs +++ b/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs @@ -6,11 +6,11 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Extensions.Actions.Fastly { - [RuleActionHandler(typeof(FastlyActionHandler))] [RuleAction( IconImage = "", IconColor = "#e23335", @@ -21,10 +21,12 @@ namespace Squidex.Extensions.Actions.Fastly { [Required] [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] + [DataType(DataType.Text)] public string ApiKey { get; set; } [Required] [Display(Name = "Service Id", Description = "The ID of the fastly service.")] + [DataType(DataType.Text)] public string ServiceId { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs new file mode 100644 index 000000000..d5e4f76b9 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.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.Fastly +{ + public sealed class FastlyPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs b/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs index 1363aa430..3781eb9de 100644 --- a/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs +++ b/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs @@ -6,11 +6,11 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Extensions.Actions.Medium { - [RuleActionHandler(typeof(MediumActionHandler))] [RuleAction( IconImage = "", IconColor = "#00ab6c", @@ -21,26 +21,37 @@ namespace Squidex.Extensions.Actions.Medium { [Required] [Display(Name = "Access Token", Description = "The self issued access token.")] + [DataType(DataType.Text)] public string AccessToken { get; set; } [Required] [Display(Name = "Title", Description = "The title, used for the url.")] + [DataType(DataType.Text)] + [Formattable] public string Title { get; set; } [Required] [Display(Name = "Content", Description = "The content, either html or markdown.")] + [DataType(DataType.MultilineText)] + [Formattable] public string Content { get; set; } [Display(Name = "Canonical Url", Description = "The original home of this content, if it was originally published elsewhere.")] + [DataType(DataType.Text)] + [Formattable] public string CanonicalUrl { get; set; } - [Display(Name = "PublicationId", Description = "Optional publication id.")] - public string PublicationId { get; set; } - [Display(Name = "Tags", Description = "The optional comma separated list of tags.")] + [DataType(DataType.Text)] + [Formattable] public string Tags { get; set; } + [Display(Name = "Publication Id", Description = "Optional publication id.")] + [DataType(DataType.Text)] + public string PublicationId { get; set; } + [Display(Name = "Is Html", Description = "Indicates whether the content is markdown or html.")] + [DataType(DataType.Custom)] public bool IsHtml { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs b/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs new file mode 100644 index 000000000..5d8839f89 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.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.Medium +{ + public sealed class MediumPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs index 47c37eafa..44285379f 100644 --- a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs +++ b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs @@ -6,11 +6,11 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Extensions.Actions.Prerender { - [RuleActionHandler(typeof(PrerenderActionHandler))] [RuleAction( IconImage = "", IconColor = "#2c3e50", @@ -21,10 +21,13 @@ namespace Squidex.Extensions.Actions.Prerender { [Required] [Display(Name = "Token", Description = "The prerender token from your account.")] + [DataType(DataType.Text)] + [Formattable] public string Token { get; set; } [Required] [Display(Name = "Url", Description = "The url to recache.")] + [DataType(DataType.Text)] public string Url { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs new file mode 100644 index 000000000..608cb32d4 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.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.Prerender +{ + public sealed class PrerenderPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs b/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs deleted file mode 100644 index 5da96ebf5..000000000 --- a/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Infrastructure; - -namespace Squidex.Extensions.Actions -{ - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public sealed class RuleActionHandlerAttribute : Attribute - { - public Type HandlerType { get; } - - public RuleActionHandlerAttribute(Type handlerType) - { - Guard.NotNull(handlerType, nameof(handlerType)); - - HandlerType = handlerType; - - if (!typeof(IRuleActionHandler).IsAssignableFrom(handlerType)) - { - throw new ArgumentException($"Handler type must implement {typeof(IRuleActionHandler)}.", nameof(handlerType)); - } - } - } -} diff --git a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs deleted file mode 100644 index dac6d42dc..000000000 --- a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; - -namespace Squidex.Extensions.Actions -{ - public static class RuleElementRegistry - { - private const string ActionSuffix = "Action"; - private const string ActionSuffixV2 = "Action"; - private static readonly HashSet ActionHandlerTypes = new HashSet(); - private static readonly Dictionary ActionTypes = new Dictionary(); - - public static IReadOnlyDictionary Triggers - { - get { return TriggerTypes.All; } - } - - public static IReadOnlyDictionary Actions - { - get { return ActionTypes; } - } - - public static IReadOnlyCollection ActionHandlers - { - get { return ActionHandlerTypes; } - } - - static RuleElementRegistry() - { - var actionTypes = - typeof(RuleElementRegistry).Assembly - .GetTypes() - .Where(x => typeof(RuleAction).IsAssignableFrom(x)) - .Where(x => x.GetCustomAttribute() != null) - .Where(x => x.GetCustomAttribute() != null) - .ToList(); - - foreach (var actionType in actionTypes) - { - var name = GetActionName(actionType); - - var metadata = actionType.GetCustomAttribute(); - - ActionTypes[name] = - new RuleElement - { - Type = actionType, - Display = metadata.Display, - Description = metadata.Description, - IconColor = metadata.IconColor, - IconImage = metadata.IconImage, - ReadMore = metadata.ReadMore - }; - - ActionHandlerTypes.Add(actionType.GetCustomAttribute().HandlerType); - } - } - - public static TypeNameRegistry MapRuleActions(this TypeNameRegistry typeNameRegistry) - { - foreach (var actionType in ActionTypes.Values) - { - typeNameRegistry.Map(actionType.Type, actionType.Type.Name); - } - - return typeNameRegistry; - } - - private static string GetActionName(Type type) - { - return type.TypeName(false, ActionSuffix, ActionSuffixV2); - } - } -} diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs b/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs index 792a0f3fc..9b1382530 100644 --- a/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs +++ b/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs @@ -7,12 +7,12 @@ using System; using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; namespace Squidex.Extensions.Actions.Slack { - [RuleActionHandler(typeof(SlackActionHandler))] [RuleAction( IconImage = "", IconColor = "#5c3a58", @@ -24,10 +24,13 @@ namespace Squidex.Extensions.Actions.Slack [AbsoluteUrl] [Required] [Display(Name = "Webhook Url", Description = "The slack webhook url.")] + [DataType(DataType.Text)] public Uri WebhookUrl { get; set; } [Required] [Display(Name = "Text", Description = "The text that is sent as message to slack.")] + [DataType(DataType.MultilineText)] + [Formattable] public string Text { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs b/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs new file mode 100644 index 000000000..15549a349 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.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.Slack +{ + public sealed class SlackPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/TriggerTypes.cs b/extensions/Squidex.Extensions/Actions/TriggerTypes.cs deleted file mode 100644 index 731a280f9..000000000 --- a/extensions/Squidex.Extensions/Actions/TriggerTypes.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// 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/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs b/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs index a16f98c6f..67bb45e63 100644 --- a/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs +++ b/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs @@ -6,11 +6,11 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; namespace Squidex.Extensions.Actions.Twitter { - [RuleActionHandler(typeof(TweetActionHandler))] [RuleAction( IconImage = "", IconColor = "#1da1f2", @@ -21,14 +21,18 @@ namespace Squidex.Extensions.Actions.Twitter { [Required] [Display(Name = "Access Token", Description = " The generated access token.")] + [DataType(DataType.Text)] public string AccessToken { get; set; } [Required] [Display(Name = "Access Secret", Description = " The generated access secret.")] + [DataType(DataType.Text)] public string AccessSecret { get; set; } [Required] [Display(Name = "Text", Description = "The text that is sent as tweet to twitter.")] + [DataType(DataType.MultilineText)] + [Formattable] public string Text { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs b/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs new file mode 100644 index 000000000..2d987b3c4 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// 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.Twitter +{ + public sealed class TwitterPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.Configure( + config.GetSection("twitter")); + + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs b/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs index e2c260b11..4c129531e 100644 --- a/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs +++ b/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs @@ -7,12 +7,11 @@ using System; using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Infrastructure; namespace Squidex.Extensions.Actions.Webhook { - [RuleActionHandler(typeof(WebhookActionHandler))] [RuleAction( IconImage = "", IconColor = "#4bb958", @@ -21,12 +20,14 @@ namespace Squidex.Extensions.Actions.Webhook ReadMore = "https://en.wikipedia.org/wiki/Webhook")] public sealed class WebhookAction : RuleAction { - [AbsoluteUrl] [Required] - [Display(Name = "Url", Description = "he url to the webhook.")] + [Display(Name = "Url", Description = "The url to the webhook.")] + [DataType(DataType.Text)] + [Formattable] public Uri Url { get; set; } [Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the signature.")] + [DataType(DataType.Text)] public string SharedSecret { get; set; } } } diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs b/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs new file mode 100644 index 000000000..e25857123 --- /dev/null +++ b/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.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.Webhook +{ + public sealed class WebhookPlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs b/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs new file mode 100644 index 000000000..bf586a836 --- /dev/null +++ b/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Plugins; + +namespace Squidex.Extensions.Samples +{ + public sealed class MemoryAssetStorePlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + var storeType = config.GetValue("assetStore:type"); + + if (string.Equals(storeType, "Memory", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingletonAs() + .As(); + } + } + } +} diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj index d365c5a6d..0940d8a2a 100644 --- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -3,17 +3,13 @@ netstandard2.0 7.3 - - full - True - - + diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs index f4049da06..6154cff13 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs @@ -12,12 +12,12 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Schemas { - public static class FieldRegistry + public class FieldRegistry : ITypeProvider { private const string Suffix = "Properties"; private const string SuffixOld = "FieldProperties"; - public static TypeNameRegistry MapFields(this TypeNameRegistry typeNameRegistry) + public void Map(TypeNameRegistry typeNameRegistry) { var types = typeof(FieldRegistry).Assembly.GetTypes().Where(x => typeof(FieldProperties).IsAssignableFrom(x) && !x.IsAbstract); @@ -32,8 +32,6 @@ namespace Squidex.Domain.Apps.Core.Schemas typeNameRegistry.MapObsolete(type, type.TypeName(false, SuffixOld)); } } - - return typeNameRegistry; } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs index 6f4f99495..83b058a77 100644 --- a/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs +++ b/src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs @@ -7,9 +7,11 @@ using System.Reflection; +#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. + namespace Squidex.Domain.Apps.Core { - public static class SquidexCoreModel + public sealed class SquidexCoreModel { public static readonly Assembly Assembly = typeof(SquidexCoreModel).Assembly; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs new file mode 100644 index 000000000..1113bcfbb --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class DependencyInjectionExtensions + { + public static IServiceCollection AddRuleAction(this IServiceCollection services) where THandler : class, IRuleActionHandler where TAction : RuleAction + { + services.AddSingletonAs() + .As(); + + services.AddSingleton(new RuleActionRegistration(typeof(TAction))); + + return services; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs new file mode 100644 index 000000000..296dcb840 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.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 +{ + [AttributeUsage(AttributeTargets.Property)] + public sealed class FormattableAttribute : Attribute + { + } +} diff --git a/extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs similarity index 94% rename from extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs rename to src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs index c5fa0343f..31c1f2368 100644 --- a/extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs @@ -7,7 +7,7 @@ using System; -namespace Squidex.Extensions.Actions +namespace Squidex.Domain.Apps.Core.HandleRules { [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class RuleActionAttribute : Attribute diff --git a/extensions/Squidex.Extensions/Actions/RuleElement.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs similarity index 75% rename from extensions/Squidex.Extensions/Actions/RuleElement.cs rename to src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs index 6d7244c01..97a9110d1 100644 --- a/extensions/Squidex.Extensions/Actions/RuleElement.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs @@ -6,10 +6,11 @@ // ========================================================================== using System; +using System.Collections.Generic; -namespace Squidex.Extensions.Actions +namespace Squidex.Domain.Apps.Core.HandleRules { - public sealed class RuleElement + public sealed class RuleActionDefinition { public Type Type { get; set; } @@ -22,5 +23,7 @@ namespace Squidex.Extensions.Actions public string Display { get; set; } public string Description { get; set; } + + public List Properties { get; } = new List(); } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs new file mode 100644 index 000000000..1611e76ea --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleActionProperty + { + public RuleActionPropertyEditor Editor { get; set; } + + public string Name { get; set; } + + public string Display { get; set; } + + public string Description { get; set; } + + public bool IsFormattable { get; set; } + + public bool IsRequired { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs new file mode 100644 index 000000000..469e01a35 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public enum RuleActionPropertyEditor + { + Checkbox, + Email, + Number, + Password, + Text, + TextArea, + Url + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs new file mode 100644 index 000000000..2d0477228 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.HandleRules +{ + public sealed class RuleActionRegistration + { + public Type ActionType { get; } + + internal RuleActionRegistration(Type actionType) + { + Guard.NotNull(actionType, nameof(actionType)); + + ActionType = actionType; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs index 31228cadd..ac9789a9f 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs @@ -7,17 +7,151 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Reflection; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules; using Squidex.Infrastructure; +#pragma warning disable RECS0033 // Convert 'if' to '||' expression + namespace Squidex.Domain.Apps.Core.HandleRules { - public static class RuleRegistry + public sealed class RuleRegistry : ITypeProvider { - public static TypeNameRegistry MapRules(this TypeNameRegistry typeNameRegistry) + private const string ActionSuffix = "Action"; + private const string ActionSuffixV2 = "ActionV2"; + private readonly Dictionary actionTypes = new Dictionary(); + + public IReadOnlyDictionary Actions { + get { return actionTypes; } + } + + public RuleRegistry(IEnumerable registrations = null) + { + if (registrations != null) + { + foreach (var registration in registrations) + { + Add(registration.ActionType); + } + } + } + + public void Add() where T : RuleAction + { + Add(typeof(T)); + } + + private void Add(Type actionType) + { + var metadata = actionType.GetCustomAttribute(); + + if (metadata == null) + { + return; + } + + var name = GetActionName(actionType); + + var definition = + new RuleActionDefinition + { + Type = actionType, + Display = metadata.Display, + Description = metadata.Description, + IconColor = metadata.IconColor, + IconImage = metadata.IconImage, + ReadMore = metadata.ReadMore + }; + + foreach (var property in actionType.GetProperties()) + { + if (property.CanRead && property.CanWrite) + { + var actionProperty = new RuleActionProperty { Name = property.Name.ToCamelCase(), Display = property.Name }; + + var display = property.GetCustomAttribute(); + + if (!string.IsNullOrWhiteSpace(display?.Name)) + { + actionProperty.Display = display.Name; + } + + if (!string.IsNullOrWhiteSpace(display?.Description)) + { + actionProperty.Description = display.Description; + } + + var type = property.PropertyType; + + if ((property.GetCustomAttribute() != null || (type.IsValueType && !IsNullable(type))) && type != typeof(bool) && type != typeof(bool?)) + { + actionProperty.IsRequired = true; + } + + if (property.GetCustomAttribute() != null) + { + actionProperty.IsFormattable = true; + } + + var dataType = property.GetCustomAttribute()?.DataType; + + if (type == typeof(bool) || type == typeof(bool?)) + { + actionProperty.Editor = RuleActionPropertyEditor.Checkbox; + } + else if (type == typeof(int) || type == typeof(int?)) + { + actionProperty.Editor = RuleActionPropertyEditor.Number; + } + else if (dataType == DataType.Url) + { + actionProperty.Editor = RuleActionPropertyEditor.Url; + } + else if (dataType == DataType.Password) + { + actionProperty.Editor = RuleActionPropertyEditor.Password; + } + else if (dataType == DataType.EmailAddress) + { + actionProperty.Editor = RuleActionPropertyEditor.Email; + } + else if (dataType == DataType.MultilineText) + { + actionProperty.Editor = RuleActionPropertyEditor.TextArea; + } + else + { + actionProperty.Editor = RuleActionPropertyEditor.Text; + } + + definition.Properties.Add(actionProperty); + } + } + + actionTypes[name] = definition; + } + + private static bool IsNullable(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + private static string GetActionName(Type type) + { + return type.TypeName(false, ActionSuffix, ActionSuffixV2); + } + + public void Map(TypeNameRegistry typeNameRegistry) + { + foreach (var actionType in actionTypes.Values) + { + typeNameRegistry.Map(actionType.Type, actionType.Type.Name); + } + var eventTypes = typeof(EnrichedEvent).Assembly.GetTypes().Where(x => typeof(EnrichedEvent).IsAssignableFrom(x) && !x.IsAbstract); var addedTypes = new HashSet(); @@ -39,8 +173,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules typeNameRegistry.Map(type, type.Name); } } - - return typeNameRegistry; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 52e1d0d86..333692707 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs b/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs new file mode 100644 index 000000000..38e3c0b0c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Reflection; + +#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. + +namespace Squidex.Domain.Apps.Core +{ + public static class SquidexCoreOperations + { + public static readonly Assembly Assembly = typeof(SquidexCoreOperations).Assembly; + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index 2972cd648..fff4b75e7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent foreach (var item in array) { - if (item is JsonNull n) + if (item is JsonNull) { result.Add(null); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index 189f08052..3c51b334b 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Assets.State; @@ -18,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public sealed partial class MongoAssetRepository : ISnapshotStore { - public async Task<(AssetState Value, long Version)> ReadAsync(Guid key) + async Task<(AssetState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) { using (Profiler.TraceMethod()) { @@ -35,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) + async Task ISnapshotStore.WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) { using (Profiler.TraceMethod()) { @@ -48,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - Task ISnapshotStore.ReadAllAsync(Func callback) + Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) { throw new NotSupportedException(); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index affc3bcb5..bd68f23b6 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -10,69 +10,86 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MongoDB.Bson; using MongoDB.Driver; +using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { internal class MongoContentCollection : MongoRepositoryBase { private readonly IAppProvider appProvider; - private readonly string collectionName; + private readonly IJsonSerializer serializer; - protected IJsonSerializer Serializer { get; } - - public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider, string collectionName) + public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) : base(database) { - this.collectionName = collectionName; - this.appProvider = appProvider; - Serializer = serializer; + this.serializer = serializer; } - protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) { - await collection.Indexes.CreateOneAsync( - new CreateIndexModel(Index.Ascending(x => x.ReferencedIds)), cancellationToken: ct); + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel(Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Status) + .Ascending(x => x.Id)), + new CreateIndexModel(Index + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.Status) + .Ascending(x => x.Id)), + new CreateIndexModel(Index + .Ascending(x => x.ScheduledAt) + .Ascending(x => x.IsDeleted)), + new CreateIndexModel(Index + .Ascending(x => x.ReferencedIds)) + }, ct); } protected override string CollectionName() { - return collectionName; + return "State_Contents"; } - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, Status[] status = null, bool useDraft = false) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, List ids, Status[] status, bool useDraft) { try { query = query.AdjustToModel(schema.SchemaDef, useDraft); - var filter = query.ToFilter(schema.Id, status); + var filter = query.ToFilter(schema.Id, ids, status); var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentItems = Collection.Find(filter) + .WithoutDraft(useDraft) .ContentTake(query) .ContentSkip(query) .ContentSort(query) - .Not(x => x.DataText) .ToListAsync(); await Task.WhenAll(contentItems, contentCount); foreach (var entity in contentItems.Result) { - entity.ParseData(schema.SchemaDef, Serializer); + entity.ParseData(schema.SchemaDef, serializer); } return ResultList.Create(contentCount.Result, contentItems.Result); @@ -90,14 +107,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[] status = null) + public async Task> QueryAsync(IAppEntity app, HashSet ids, Status[] status, bool useDraft) { - var find = - status != null && status.Length > 0 ? - Collection.Find(x => x.IndexedAppId == app.Id && ids.Contains(x.Id) && x.IsDeleted != true && status.Contains(x.Status)) : - Collection.Find(x => x.IndexedAppId == app.Id && ids.Contains(x.Id)); + var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status)); - var contentItems = await find.Not(x => x.DataText).ToListAsync(); + var contentItems = await find.WithoutDraft(useDraft).ToListAsync(); var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList(); var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x))); @@ -110,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents if (schema != null) { - entity.ParseData(schema.SchemaDef, Serializer); + entity.ParseData(schema.SchemaDef, serializer); result.Add((entity, schema)); } @@ -119,26 +133,95 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return result; } - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, Status[] status = null) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, Status[] status, bool useDraft) { - var find = - status != null && status.Length > 0 ? - Collection.Find(x => x.IndexedSchemaId == schema.Id && ids.Contains(x.Id) && x.IsDeleted != true && status.Contains(x.Status)) : - Collection.Find(x => x.IndexedSchemaId == schema.Id && ids.Contains(x.Id)); + var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status)); - var contentItems = find.Not(x => x.DataText).ToListAsync(); + var contentItems = find.WithoutDraft(useDraft).ToListAsync(); var contentCount = find.CountDocumentsAsync(); await Task.WhenAll(contentItems, contentCount); foreach (var entity in contentItems.Result) { - entity.ParseData(schema.SchemaDef, Serializer); + entity.ParseData(schema.SchemaDef, serializer); } return ResultList.Create(contentCount.Result, contentItems.Result); } + public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, Status[] status, bool useDraft) + { + var find = Collection.Find(FilterFactory.Build(schema.Id, id, status)); + + var contentEntity = await find.WithoutDraft(useDraft).FirstOrDefaultAsync(); + + contentEntity?.ParseData(schema.SchemaDef, serializer); + + return contentEntity; + } + + public Task QueryScheduledWithoutDataAsync(Instant now, Func callback) + { + return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) + .Not(x => x.DataByIds) + .Not(x => x.DataDraftByIds) + .ForEachAsync(c => + { + callback(c); + }); + } + + public async Task> QueryIdsAsync(Guid appId, ISchemaEntity schema, FilterNode filterNode) + { + var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id); + + var contentEntities = + await Collection.Find(filter).Only(x => x.Id) + .ToListAsync(); + + return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); + } + + public async Task> QueryIdsAsync(Guid appId) + { + var contentEntities = + await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id) + .ToListAsync(); + + return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); + } + + public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func> getSchema) + { + var contentEntity = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (contentEntity != null) + { + var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); + + contentEntity.ParseData(schema.SchemaDef, serializer); + + return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); + } + + return (null, EtagVersion.NotFound); + } + + public Task ReadAllAsync(Func callback, Func> getSchema, CancellationToken ct = default) + { + return Collection.Find(new BsonDocument()).ForEachPipelineAsync(async contentEntity => + { + var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); + + contentEntity.ParseData(schema.SchemaDef, serializer); + + await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); + }, ct); + } + public Task CleanupAsync(Guid id) { return Collection.UpdateManyAsync( @@ -152,5 +235,31 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { return Collection.DeleteOneAsync(x => x.Id == id); } + + public async Task UpsertAsync(MongoContentEntity content, long oldVersion) + { + try + { + await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version) + .FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex); + } + } + else + { + throw; + } + } + } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs deleted file mode 100644 index bdec55cab..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs +++ /dev/null @@ -1,152 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using NodaTime; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Queries; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - internal sealed class MongoContentDraftCollection : MongoContentCollection - { - public MongoContentDraftCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) - : base(database, serializer, appProvider, "State_Content_Draft") - { - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - await collection.Indexes.CreateManyAsync( - new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.Id) - .Ascending(x => x.IsDeleted)), - new CreateIndexModel( - Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.Id) - .Ascending(x => x.IsDeleted)), - new CreateIndexModel( - Index - .Text(x => x.DataText) - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status)) - }, ct); - - await base.SetupCollectionAsync(collection, ct); - } - - public async Task> QueryIdsAsync(Guid appId, ISchemaEntity schema, FilterNode filterNode) - { - var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id); - - var contentEntities = - await Collection.Find(filter).Only(x => x.Id) - .ToListAsync(); - - return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - - public async Task> QueryIdsAsync(Guid appId) - { - var contentEntities = - await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id) - .ToListAsync(); - - return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); - } - - public Task QueryScheduledWithoutDataAsync(Instant now, Func callback) - { - return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) - .Not(x => x.DataByIds) - .Not(x => x.DataDraftByIds) - .Not(x => x.DataText) - .ForEachAsync(c => - { - callback(c); - }); - } - - public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) - { - var contentEntity = - await Collection.Find(x => x.IndexedSchemaId == schema.Id && x.Id == id && x.IsDeleted != true).Not(x => x.DataText) - .FirstOrDefaultAsync(); - - contentEntity?.ParseData(schema.SchemaDef, Serializer); - - return contentEntity; - } - - public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func> getSchema) - { - var contentEntity = - await Collection.Find(x => x.Id == key).Not(x => x.DataText) - .FirstOrDefaultAsync(); - - if (contentEntity != null) - { - var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); - - contentEntity.ParseData(schema.SchemaDef, Serializer); - - return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); - } - - return (null, EtagVersion.NotFound); - } - - public async Task UpsertAsync(MongoContentEntity content, long oldVersion) - { - try - { - content.DataText = content.DataDraftByIds.ToFullText(); - - await Collection.ReplaceOneAsync(x => x.Id == content.Id && x.Version == oldVersion, content, Upsert); - } - catch (MongoWriteException ex) - { - if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await Collection.Find(x => x.Id == content.Id).Only(x => x.Id, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - throw new InconsistentStateException(existingVersion["vs"].AsInt64, oldVersion, ex); - } - } - else - { - throw; - } - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index a2346db31..9c8f3eba7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -69,10 +69,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonJson] public ScheduleJob ScheduleJob { get; set; } - [BsonIgnoreIfDefault] - [BsonElement("dt")] - public string DataText { get; set; } - [BsonRequired] [BsonElement("ai")] public NamedId AppId { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs deleted file mode 100644 index c210297a8..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// 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 MongoDB.Driver; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Json; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents -{ - internal sealed class MongoContentPublishedCollection : MongoContentCollection - { - public MongoContentPublishedCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) - : base(database, serializer, appProvider, "State_Content_Published") - { - } - - protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) - { - await collection.Indexes.CreateManyAsync( - new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.Id)), - new CreateIndexModel( - Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.Id)), - new CreateIndexModel( - Index - .Text(x => x.DataText) - .Ascending(x => x.IndexedSchemaId)) - }, ct); - - await base.SetupCollectionAsync(collection, ct); - } - - public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) - { - var contentEntity = - await Collection.Find(x => x.IndexedSchemaId == schema.Id && x.Id == id).Not(x => x.DataText) - .FirstOrDefaultAsync(); - - contentEntity?.ParseData(schema.SchemaDef, Serializer); - - return contentEntity; - } - - public Task UpsertAsync(MongoContentEntity content) - { - content.DataText = content.DataByIds.ToFullText(); - content.DataDraftByIds = null; - content.ScheduleJob = null; - content.ScheduledAt = null; - - return Collection.ReplaceOneAsync(x => x.Id == content.Id, content, new UpdateOptions { IsUpsert = true }); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 8adc0a065..2a74ffe74 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; @@ -28,86 +29,90 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents private readonly IMongoDatabase database; private readonly IAppProvider appProvider; private readonly IJsonSerializer serializer; - private readonly MongoContentDraftCollection contentsDraft; - private readonly MongoContentPublishedCollection contentsPublished; + private readonly ITextIndexer indexer; + private readonly MongoContentCollection contents; - public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer) + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(serializer, nameof(serializer)); + Guard.NotNull(indexer, nameof(ITextIndexer)); this.appProvider = appProvider; - + this.database = database; + this.indexer = indexer; this.serializer = serializer; - contentsDraft = new MongoContentDraftCollection(database, serializer, appProvider); - contentsPublished = new MongoContentPublishedCollection(database, serializer, appProvider); - - this.database = database; + contents = new MongoContentCollection(database, serializer, appProvider); } public Task InitializeAsync(CancellationToken ct = default) { - return Task.WhenAll(contentsDraft.InitializeAsync(ct), contentsPublished.InitializeAsync(ct)); + return contents.InitializeAsync(ct); } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query) { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(status, nameof(status)); + Guard.NotNull(query, nameof(query)); + using (Profiler.TraceMethod("QueryAsyncByQuery")) { - if (RequiresPublished(status)) - { - return await contentsPublished.QueryAsync(app, schema, query); - } - else + var useDraft = UseDraft(status); + + var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, useDraft); + + if (fullTextIds?.Count == 0) { - return await contentsDraft.QueryAsync(app, schema, query, status, true); + return ResultList.Create(0); } + + return await contents.QueryAsync(app, schema, query, fullTextIds, status, true); } } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids) { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(status, nameof(status)); + Guard.NotNull(ids, nameof(ids)); + using (Profiler.TraceMethod("QueryAsyncByIds")) { - if (RequiresPublished(status)) - { - return await contentsPublished.QueryAsync(app, schema, ids); - } - else - { - return await contentsDraft.QueryAsync(app, schema, ids, status); - } + var useDraft = UseDraft(status); + + return await contents.QueryAsync(app, schema, ids, status, useDraft); } } public async Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids) { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(status, nameof(status)); + Guard.NotNull(ids, nameof(ids)); + using (Profiler.TraceMethod("QueryAsyncByIdsWithoutSchema")) { - if (RequiresPublished(status)) - { - return await contentsPublished.QueryAsync(app, ids); - } - else - { - return await contentsDraft.QueryAsync(app, ids, status); - } + var useDraft = UseDraft(status); + + return await contents.QueryAsync(app, ids, status, useDraft); } } public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id) { + Guard.NotNull(app, nameof(app)); + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(status, nameof(status)); + using (Profiler.TraceMethod()) { - if (RequiresPublished(status)) - { - return await contentsPublished.FindContentAsync(app, schema, id); - } - else - { - return await contentsDraft.FindContentAsync(app, schema, id); - } + var useDraft = UseDraft(status); + + return await contents.FindContentAsync(app, schema, id, status, useDraft); } } @@ -115,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - return await contentsDraft.QueryIdsAsync(appId, await appProvider.GetSchemaAsync(appId, schemaId), filterNode); + return await contents.QueryIdsAsync(appId, await appProvider.GetSchemaAsync(appId, schemaId), filterNode); } } @@ -123,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - return await contentsDraft.QueryIdsAsync(appId); + return await contents.QueryIdsAsync(appId); } } @@ -131,22 +136,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - await contentsDraft.QueryScheduledWithoutDataAsync(now, callback); + await contents.QueryScheduledWithoutDataAsync(now, callback); } } - public Task RemoveAsync(Guid appId) - { - return Task.WhenAll( - contentsDraft.RemoveAsync(appId), - contentsPublished.RemoveAsync(appId)); - } - public Task ClearAsync() { - return Task.WhenAll( - contentsDraft.ClearAsync(), - contentsPublished.ClearAsync()); + return contents.ClearAsync(); } public Task DeleteArchiveAsync() @@ -154,9 +150,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return database.DropCollectionAsync("States_Contents_Archive"); } - private static bool RequiresPublished(Status[] status) + private static bool UseDraft(Status[] status) { - return status?.Length == 1 && status[0] == Status.Published; + return status.Length != 1 || status[0] != Status.Published; } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 427629d1d..bb072df70 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -33,16 +33,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents protected Task On(AssetDeleted @event) { - return Task.WhenAll( - contentsDraft.CleanupAsync(@event.AssetId), - contentsPublished.CleanupAsync(@event.AssetId)); + return contents.CleanupAsync(@event.AssetId); } protected Task On(ContentDeleted @event) { - return Task.WhenAll( - contentsDraft.CleanupAsync(@event.ContentId), - contentsPublished.CleanupAsync(@event.ContentId)); + return contents.CleanupAsync(@event.ContentId); } Task IEventConsumer.ClearAsync() diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index 32f38e982..c45d01aa5 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -6,8 +6,8 @@ // ========================================================================== using System; +using System.Threading; using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; @@ -19,15 +19,31 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { public partial class MongoContentRepository : ISnapshotStore { - public async Task<(ContentState Value, long Version)> ReadAsync(Guid key) + async Task ISnapshotStore.RemoveAsync(Guid key) { using (Profiler.TraceMethod()) { - return await contentsDraft.ReadAsync(key, GetSchemaAsync); + await contents.RemoveAsync(key); } } - public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) + async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) + { + using (Profiler.TraceMethod()) + { + await contents.ReadAllAsync(callback, GetSchemaAsync, ct); + } + } + + async Task<(ContentState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + return await contents.ReadAsync(key, GetSchemaAsync); + } + } + + async Task ISnapshotStore.WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) { using (Profiler.TraceMethod()) { @@ -58,15 +74,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents Version = newVersion }); - await contentsDraft.UpsertAsync(content, oldVersion); + await contents.UpsertAsync(content, oldVersion); - if (value.Status == Status.Published && !value.IsDeleted) + if (value.IsDeleted) { - await contentsPublished.UpsertAsync(content); + await indexer.DeleteAsync(value.SchemaId.Id, value.Id); } else { - await contentsPublished.RemoveAsync(content.Id); + await indexer.IndexAsync(value.SchemaId.Id, value.Id, value.Data, value.DataDraft); } } } @@ -82,15 +98,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return schema; } - - Task ISnapshotStore.RemoveAsync(Guid key) - { - throw new NotSupportedException(); - } - - Task ISnapshotStore.ReadAllAsync(Func callback) - { - throw new NotSupportedException(); - } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs similarity index 70% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs index 14195985f..61c256448 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs @@ -14,12 +14,13 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { - public static class FindExtensions + public static class FilterFactory { private static readonly FilterDefinitionBuilder Filter = Builders.Filter; @@ -109,33 +110,65 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors return cursor.Skip(query); } - public static FilterDefinition ToFilter(this Query query, Guid schemaId, Status[] status) + public static IFindFluent WithoutDraft(this IFindFluent cursor, bool useDraft) { - var filters = new List> + return !useDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor; + } + + public static FilterDefinition Build(Guid schemaId, Guid id, Status[] status) + { + return CreateFilter(null, schemaId, new List { id }, status, null); + } + + public static FilterDefinition IdsByApp(Guid appId, ICollection ids, Status[] status) + { + return CreateFilter(appId, null, ids, status, null); + } + + public static FilterDefinition IdsBySchema(Guid schemaId, ICollection ids, Status[] status) + { + return CreateFilter(null, schemaId, ids, status, null); + } + + public static FilterDefinition ToFilter(this Query query, Guid schemaId, ICollection ids, Status[] status) + { + return CreateFilter(null, schemaId, ids, status, query); + } + + private static FilterDefinition CreateFilter(Guid? appId, Guid? schemaId, ICollection ids, Status[] status, Query query) + { + var filters = new List>(); + + if (appId.HasValue) { - Filter.Eq(x => x.IndexedSchemaId, schemaId), - Filter.Ne(x => x.IsDeleted, true) - }; + filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value)); + } - if (status != null) + if (schemaId.HasValue) { - filters.Add(Filter.In(x => x.Status, status)); + filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value)); } - var filter = query.BuildFilter(); + filters.Add(Filter.Ne(x => x.IsDeleted, true)); + filters.Add(Filter.In(x => x.Status, status)); - if (filter.Filter != null) + if (ids != null && ids.Count > 0) { - if (filter.Last) + if (ids.Count > 1) { - filters.Add(filter.Filter); + filters.Add(Filter.In(x => x.Id, ids)); } else { - filters.Insert(0, filter.Filter); + filters.Add(Filter.Eq(x => x.Id, ids.First())); } } + if (query.Filter != null) + { + filters.Add(query.Filter.BuildFilter()); + } + return Filter.And(filters); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs index 6b5e6cd77..5a39bb706 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs @@ -9,6 +9,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using MongoDB.Bson.Serialization; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History.Repositories; @@ -18,9 +20,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History { public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository { - public MongoHistoryEventRepository(IMongoDatabase database) + public MongoHistoryEventRepository(IMongoDatabase database, IOptions options) : base(database) { + if (options.Value.IsCosmosDb) + { + var classMap = BsonClassMap.RegisterClassMap(); + + classMap.MapProperty(x => x.Created) + .SetElementName("_ts"); + classMap.AutoMap(); + } } protected override string CollectionName() diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index bae5d32f0..b8c6ea495 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index c1ee42557..d91a5ff4f 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -369,11 +369,6 @@ namespace Squidex.Domain.Apps.Entities.Apps return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; } - protected override AppState OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - public Task> GetStateAsync() { return J.AsTask(Snapshot); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs index ffd7d333a..3d281b676 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs @@ -40,11 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes public Task ReserveAppAsync(Guid appId, string name) { - var canReserve = - !State.Apps.ContainsKey(name) && - !State.Apps.Any(x => x.Value == appId) && - !reservedIds.Contains(appId) && - !reservedNames.Contains(name); + var canReserve = !IsInUse(appId, name) && !IsReserved(appId, name); if (canReserve) { @@ -55,6 +51,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes return Task.FromResult(canReserve); } + private bool IsInUse(Guid appId, string name) + { + return State.Apps.ContainsKey(name) || State.Apps.Any(x => x.Value == appId); + } + + private bool IsReserved(Guid appId, string name) + { + return reservedIds.Contains(appId) || reservedNames.Contains(name); + } + public Task RemoveReservationAsync(Guid appId, string name) { reservedIds.Remove(appId); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 36d8ce960..ff4f8c293 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State IsArchived = true; } - public AppState Apply(Envelope @event) + public override AppState Apply(Envelope @event) { var payload = (SquidexEvent)@event.Payload; diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index 8173494e3..7ba5ea4ab 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -165,11 +165,6 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - protected override AssetState OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - public Task> GetStateAsync(long version = EtagVersion.Any) { return J.AsTask(GetSnapshot(version)); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index 6e6ca7dae..928fc292d 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { await RestoreTagsAsync(appId, reader); - await RebuildManyAsync(assetIds, id => RebuildAsync(id, (e, s) => s.Apply(e))); + await RebuildManyAsync(assetIds, id => RebuildAsync(id)); } private async Task RestoreTagsAsync(Guid appId, BackupReader reader) @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { try { - await assetStore.UploadAsync(assetId.ToString(), fileVersion, null, stream); + await assetStore.UploadAsync(assetId.ToString(), fileVersion, null, stream, true); } catch (AssetAlreadyExistsException) { diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index e9ebd33ec..278c4e666 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State IsDeleted = true; } - public AssetState Apply(Envelope @event) + public override AssetState Apply(Envelope @event) { var payload = (SquidexEvent)@event.Payload; diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs index f1bff2c44..fe6a484ec 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Backup currentTask.Token.ThrowIfCancellationRequested(); - await assetStore.UploadAsync(job.Id.ToString(), 0, null, stream, currentTask.Token); + await assetStore.UploadAsync(job.Id.ToString(), 0, null, stream, false, currentTask.Token); } job.Status = JobStatus.Completed; diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs index 3c2cb4354..6e711347e 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Backup @@ -39,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Backup } } - protected async Task RebuildAsync(Guid key, Func, TState, TState> func) where TState : IDomainState, new() + protected async Task RebuildAsync(Guid key) where TState : IDomainState, new() { var state = new TState { @@ -48,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Backup var persistence = store.WithSnapshotsAndEventSourcing(typeof(TGrain), key, (TState s) => state = s, e => { - state = func(e, state); + state = state.Apply(e); state.Version++; }); diff --git a/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs b/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs index c5e8184ac..b9f9eb62e 100644 --- a/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs +++ b/src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs @@ -5,9 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure.EventSourcing; + namespace Squidex.Domain.Apps.Entities.Comments.State { public sealed class CommentsState : DomainObjectState { + public override CommentsState Apply(Envelope @event) + { + return this; + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs index 5f4bef81d..d62e9c54c 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var contentIds = contentIdsBySchemaId.Values.SelectMany(x => x); - return RebuildManyAsync(contentIds, id => RebuildAsync(id, (e, s) => s.Apply(e))); + return RebuildManyAsync(contentIds, id => RebuildAsync(id)); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index f934c0deb..5adf76236 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -296,11 +296,6 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - protected override ContentState OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - private async Task CreateContext(Guid appId, Guid schemaId, Guid contentId, Func message) { var operationContext = diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs index d1885adcd..051f66a7a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs @@ -22,15 +22,15 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ContentSchedulerGrain : Grain, IContentSchedulerGrain, IRemindable { - private readonly Lazy contentRepository; - private readonly Lazy commandBus; + private readonly IContentRepository contentRepository; + private readonly ICommandBus commandBus; private readonly IClock clock; private readonly ISemanticLog log; private TaskScheduler scheduler; public ContentSchedulerGrain( - Lazy contentRepository, - Lazy commandBus, + IContentRepository contentRepository, + ICommandBus commandBus, IClock clock, ISemanticLog log) { @@ -39,9 +39,11 @@ namespace Squidex.Domain.Apps.Entities.Contents Guard.NotNull(clock, nameof(clock)); Guard.NotNull(log, nameof(log)); - this.contentRepository = contentRepository; - this.commandBus = commandBus; this.clock = clock; + + this.commandBus = commandBus; + this.contentRepository = contentRepository; + this.log = log; } @@ -66,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var now = clock.GetCurrentInstant(); - return contentRepository.Value.QueryScheduledWithoutDataAsync(now, content => + return contentRepository.QueryScheduledWithoutDataAsync(now, content => { return Dispatch(async () => { @@ -78,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var command = new ChangeContentStatus { ContentId = content.Id, Status = job.Status, Actor = job.ScheduledBy, JobId = job.Id }; - await commandBus.Value.PublishAsync(command); + await commandBus.PublishAsync(command); } } catch (Exception ex) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 56ac77ede..8f5c2f0bd 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -30,7 +30,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id); Task QueryScheduledWithoutDataAsync(Instant now, Func callback); - - Task RemoveAsync(Guid appId); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 56936bb0a..fd375704b 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State IsDeleted = true; } - public ContentState Apply(Envelope @event) + public override ContentState Apply(Envelope @event) { var payload = (SquidexEvent)@event.Payload; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs new file mode 100644 index 000000000..e5bcfa60c --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class GrainTextIndexer : ITextIndexer + { + private readonly IGrainFactory grainFactory; + private readonly ISemanticLog log; + + public GrainTextIndexer(IGrainFactory grainFactory, ISemanticLog log) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + Guard.NotNull(log, nameof(log)); + + this.grainFactory = grainFactory; + + this.log = log; + } + + public async Task DeleteAsync(Guid schemaId, Guid id) + { + var index = grainFactory.GetGrain(schemaId); + + using (Profiler.TraceMethod()) + { + try + { + await index.DeleteAsync(id); + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "DeleteTextEntry") + .WriteProperty("status", "Failed")); + } + } + } + + public async Task IndexAsync(Guid schemaId, Guid id, NamedContentData data, NamedContentData dataDraft) + { + var index = grainFactory.GetGrain(schemaId); + + using (Profiler.TraceMethod()) + { + try + { + if (data != null) + { + await index.IndexAsync(id, new IndexData { Data = data }); + } + + if (dataDraft != null) + { + await index.IndexAsync(id, new IndexData { Data = dataDraft, IsDraft = true }); + } + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "UpdateTextEntry") + .WriteProperty("status", "Failed")); + } + } + } + + public async Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, bool useDraft = false) + { + if (string.IsNullOrWhiteSpace(queryText)) + { + return null; + } + + var index = grainFactory.GetGrain(schemaId); + + using (Profiler.TraceMethod()) + { + var context = CreateContext(app, useDraft); + + return await index.SearchAsync(queryText, context); + } + } + + private static SearchContext CreateContext(IAppEntity app, bool useDraft) + { + var languages = new HashSet(app.LanguagesConfig.Select(x => x.Key)); + + return new SearchContext { Languages = languages, IsDraft = useDraft }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs new file mode 100644 index 000000000..b350b8ee9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Apps; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public interface ITextIndexer + { + Task DeleteAsync(Guid schemaId, Guid id); + + Task IndexAsync(Guid schemaId, Guid id, NamedContentData data, NamedContentData dataDraft); + + Task> SearchAsync(string queryText, IAppEntity app, Guid schemaId, bool useDraft = false); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs new file mode 100644 index 000000000..dd1d4c5c8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public interface ITextIndexerGrain : IGrainWithGuidKey + { + Task DeleteAsync(Guid id); + + Task IndexAsync(Guid id, J data); + + Task> SearchAsync(string queryText, SearchContext context); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexData.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexData.cs new file mode 100644 index 000000000..7911be4a7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/IndexData.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class IndexData + { + public NamedContentData Data { get; set; } + + public bool IsDraft { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs new file mode 100644 index 000000000..8bd41df9c --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Util; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class MultiLanguageAnalyzer : AnalyzerWrapper + { + private readonly StandardAnalyzer fallbackAnalyzer; + private readonly Dictionary analyzers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public MultiLanguageAnalyzer(LuceneVersion version) + : base(PER_FIELD_REUSE_STRATEGY) + { + fallbackAnalyzer = new StandardAnalyzer(version); + + foreach (var type in typeof(StandardAnalyzer).Assembly.GetTypes()) + { + if (typeof(Analyzer).IsAssignableFrom(type)) + { + var language = type.Namespace.Split('.').Last(); + + if (language.Length == 2) + { + try + { + var analyzer = Activator.CreateInstance(type, version); + + analyzers[language] = (Analyzer)analyzer; + } + catch (MissingMethodException) + { + continue; + } + } + } + } + } + + protected override Analyzer GetWrappedAnalyzer(string fieldName) + { + if (fieldName.Length > 0) + { + var analyzer = analyzers.GetOrDefault(fieldName.Substring(0, 2)) ?? fallbackAnalyzer; + + return analyzer; + } + else + { + return fallbackAnalyzer; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs new file mode 100644 index 000000000..d847ccf13 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs @@ -0,0 +1,94 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; +using Lucene.Net.Index; +using Squidex.Infrastructure.Assets; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public static class PersistenceHelper + { + private const string ArchiveFile = "Archive.zip"; + private const string LockFile = "write.lock"; + + public static async Task UploadDirectoryAsync(this IAssetStore assetStore, DirectoryInfo directory, IndexCommit commit) + { + using (var fileStream = new FileStream( + Path.Combine(directory.FullName, ArchiveFile), + FileMode.Create, + FileAccess.ReadWrite, + FileShare.None, + 4096, + FileOptions.DeleteOnClose)) + { + using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + { + foreach (var fileName in commit.FileNames) + { + var file = new FileInfo(Path.Combine(directory.FullName, fileName)); + + try + { + if (!file.Name.Equals(ArchiveFile, StringComparison.OrdinalIgnoreCase) && + !file.Name.Equals(LockFile, StringComparison.OrdinalIgnoreCase)) + { + zipArchive.CreateEntryFromFile(file.FullName, file.Name); + } + } + catch (IOException) + { + continue; + } + } + } + + fileStream.Position = 0; + + await assetStore.UploadAsync(directory.Name, 0, string.Empty, fileStream, true); + } + } + + public static async Task DownloadAsync(this IAssetStore assetStore, DirectoryInfo directory) + { + if (directory.Exists) + { + directory.Delete(true); + } + + directory.Create(); + + using (var fileStream = new FileStream( + Path.Combine(directory.FullName, ArchiveFile), + FileMode.Create, + FileAccess.ReadWrite, + FileShare.None, + 4096, + FileOptions.DeleteOnClose)) + { + try + { + await assetStore.DownloadAsync(directory.Name, 0, string.Empty, fileStream); + + fileStream.Position = 0; + + using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read, true)) + { + zipArchive.ExtractToDirectory(directory.FullName); + } + } + catch (AssetNotFoundException) + { + return; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs new file mode 100644 index 000000000..01bd8f78a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class SearchContext + { + public bool IsDraft { get; set; } + + public HashSet Languages { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs new file mode 100644 index 000000000..5e1f0e57d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs @@ -0,0 +1,297 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Lucene.Net.Analysis; +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Lucene.Net.Queries; +using Lucene.Net.QueryParsers.Classic; +using Lucene.Net.Search; +using Lucene.Net.Store; +using Lucene.Net.Util; +using Squidex.Domain.Apps.Core; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Contents.Text +{ + public sealed class TextIndexerGrain : GrainOfGuid, ITextIndexerGrain + { + private const LuceneVersion Version = LuceneVersion.LUCENE_48; + private const int MaxResults = 2000; + private const int MaxUpdates = 100; + private const string MetaId = "_id"; + private const string MetaKey = "_key"; + private const string MetaDraft = "_dd"; + private static readonly TimeSpan CommitDelay = TimeSpan.FromSeconds(30); + private static readonly Analyzer Analyzer = new MultiLanguageAnalyzer(Version); + private static readonly TermsFilter DraftFilter = new TermsFilter(new Term(MetaDraft, "1")); + private static readonly TermsFilter NoDraftFilter = new TermsFilter(new Term(MetaDraft, "0")); + private readonly SnapshotDeletionPolicy snapshotter = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); + private readonly IAssetStore assetStore; + private IDisposable timer; + private DirectoryInfo directory; + private IndexWriter indexWriter; + private IndexReader indexReader; + private IndexSearcher indexSearcher; + private QueryParser queryParser; + private HashSet currentLanguages; + private long updates; + + public TextIndexerGrain(IAssetStore assetStore) + { + Guard.NotNull(assetStore, nameof(assetStore)); + + this.assetStore = assetStore; + } + + public override async Task OnDeactivateAsync() + { + await DeactivateAsync(true); + } + + protected override async Task OnActivateAsync(Guid key) + { + directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"Index_{key}")); + + await assetStore.DownloadAsync(directory); + + var config = new IndexWriterConfig(Version, Analyzer) + { + IndexDeletionPolicy = snapshotter + }; + + indexWriter = new IndexWriter(FSDirectory.Open(directory), config); + + if (indexWriter.NumDocs > 0) + { + indexReader = indexWriter.GetReader(false); + indexSearcher = new IndexSearcher(indexReader); + } + } + + public Task DeleteAsync(Guid id) + { + indexWriter.DeleteDocuments(new Term(MetaId, id.ToString())); + + return TryFlushAsync(); + } + + public Task IndexAsync(Guid id, J data) + { + var docId = id.ToString(); + var docDraft = data.Value.IsDraft ? "1" : "0"; + var docKey = $"{docId}_{docDraft}"; + + indexWriter.DeleteDocuments(new Term(MetaKey, docKey)); + + var languages = new Dictionary(); + + void AppendText(string language, string text) + { + if (!string.IsNullOrWhiteSpace(text)) + { + var sb = languages.GetOrAddNew(language); + + if (sb.Length > 0) + { + sb.Append(" "); + } + + sb.Append(text); + } + } + + foreach (var field in data.Value.Data) + { + foreach (var fieldValue in field.Value) + { + var appendText = new Action(text => AppendText(fieldValue.Key, text)); + + AppendJsonText(fieldValue.Value, appendText); + } + } + + if (languages.Count > 0) + { + var document = new Document(); + + document.AddStringField(MetaId, docId, Field.Store.YES); + document.AddStringField(MetaKey, docKey, Field.Store.YES); + document.AddStringField(MetaDraft, docDraft, Field.Store.YES); + + foreach (var field in languages) + { + var fieldName = BuildFieldName(field.Key); + + document.AddTextField(fieldName, field.Value.ToString(), Field.Store.NO); + } + + indexWriter.AddDocument(document); + } + + return TryFlushAsync(); + } + + private static void AppendJsonText(IJsonValue value, Action appendText) + { + if (value.Type == JsonValueType.String) + { + appendText(value.ToString()); + } + else if (value is JsonArray array) + { + foreach (var item in array) + { + AppendJsonText(item, appendText); + } + } + else if (value is JsonObject obj) + { + foreach (var item in obj.Values) + { + AppendJsonText(item, appendText); + } + } + } + + public Task> SearchAsync(string queryText, SearchContext context) + { + var result = new HashSet(); + + if (!string.IsNullOrWhiteSpace(queryText)) + { + var query = BuildQuery(queryText, context); + + if (indexReader != null) + { + var filter = context.IsDraft ? DraftFilter : NoDraftFilter; + + var hits = indexSearcher.Search(query, filter, MaxResults).ScoreDocs; + + foreach (var hit in hits) + { + var document = indexReader.Document(hit.Doc); + + var idField = document.GetField(MetaId)?.GetStringValue(); + + if (idField != null && Guid.TryParse(idField, out var guid)) + { + result.Add(guid); + } + } + } + } + + return Task.FromResult(result.ToList()); + } + + private Query BuildQuery(string query, SearchContext context) + { + if (queryParser == null || !currentLanguages.SetEquals(context.Languages)) + { + var fields = + context.Languages.Select(BuildFieldName) + .Union(Enumerable.Repeat(BuildFieldName(InvariantPartitioning.Instance.Master.Key), 1)).ToArray(); + + queryParser = new MultiFieldQueryParser(Version, fields, Analyzer); + + currentLanguages = context.Languages; + } + + try + { + return queryParser.Parse(query); + } + catch (ParseException ex) + { + throw new ValidationException(ex.Message); + } + } + + private async Task TryFlushAsync() + { + updates++; + + if (updates >= MaxUpdates) + { + await FlushAsync(); + } + else + { + timer?.Dispose(); + + try + { + timer = RegisterTimer(_ => FlushAsync(), null, CommitDelay, CommitDelay); + } + catch (InvalidOperationException) + { + return; + } + } + } + + public async Task FlushAsync() + { + if (updates > 0 && indexWriter != null) + { + indexWriter.Commit(); + indexWriter.Flush(true, true); + + indexReader?.Dispose(); + indexReader = indexWriter.GetReader(false); + indexSearcher = new IndexSearcher(indexReader); + + var commit = snapshotter.Snapshot(); + try + { + await assetStore.UploadDirectoryAsync(directory, commit); + } + finally + { + snapshotter.Release(commit); + } + + updates = 0; + } + else + { + timer?.Dispose(); + } + } + + public async Task DeactivateAsync(bool deleteFolder = false) + { + await TryFlushAsync(); + + indexWriter?.Dispose(); + indexWriter = null; + + indexReader?.Dispose(); + indexReader = null; + + if (deleteFolder && directory.Exists) + { + directory.Delete(true); + } + } + + private static string BuildFieldName(string language) + { + return language; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 58b6341b5..7bafa9c44 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -10,11 +10,12 @@ using System.Runtime.Serialization; using NodaTime; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities { public abstract class DomainObjectState : Cloneable, - IDomainState, + IDomainState, IEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, @@ -42,6 +43,8 @@ namespace Squidex.Domain.Apps.Entities [DataMember] public long Version { get; set; } = EtagVersion.Empty; + public abstract T Apply(Envelope @event); + public T Clone() { return Clone(x => { }); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs index 248b09542..f6073a4f6 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -123,11 +123,6 @@ namespace Squidex.Domain.Apps.Entities.Rules } } - protected override RuleState OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - public Task> GetStateAsync() { return J.AsTask(Snapshot); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs index a12f9a744..48e354328 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.State IsDeleted = true; } - public RuleState Apply(Envelope @event) + public override RuleState Apply(Envelope @event) { var payload = (SquidexEvent)@event.Payload; diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index 167971a34..5905b66fd 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -381,11 +381,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas } } - protected override SchemaState OnEvent(Envelope @event) - { - return Snapshot.Apply(@event); - } - public Task> GetStateAsync() { return J.AsTask(Snapshot); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 800a41687..922da11a3 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State IsDeleted = true; } - public SchemaState Apply(Envelope @event) + public override SchemaState Apply(Envelope @event) { var payload = (SquidexEvent)@event.Payload; diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 6adb6a190..46f190489 100644 --- a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -16,11 +16,15 @@ - + + + + + all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs index bf9c58327..6d452a60f 100644 --- a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities { - public abstract class SquidexDomainObjectGrain : DomainObjectGrain where T : IDomainState, new() + public abstract class SquidexDomainObjectGrain : DomainObjectGrain where T : IDomainState, new() { protected SquidexDomainObjectGrain(IStore store, ISemanticLog log) : base(store, log) diff --git a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs index 425bdc4d6..56df2b4a0 100644 --- a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs +++ b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities { - public abstract class SquidexDomainObjectGrainLogSnapshots : LogSnapshotDomainObjectGrain where T : IDomainState, new() + public abstract class SquidexDomainObjectGrainLogSnapshots : LogSnapshotDomainObjectGrain where T : IDomainState, new() { protected SquidexDomainObjectGrainLogSnapshots(IStore store, ISemanticLog log) : base(store, log) diff --git a/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs b/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs index 8d82fce42..4bf944913 100644 --- a/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs +++ b/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs @@ -7,6 +7,8 @@ using System.Reflection; +#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. + namespace Squidex.Domain.Apps.Entities { public static class SquidexEntities diff --git a/src/Squidex.Domain.Apps.Events/SquidexEvents.cs b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs index d49cb8921..e90815bfd 100644 --- a/src/Squidex.Domain.Apps.Events/SquidexEvents.cs +++ b/src/Squidex.Domain.Apps.Events/SquidexEvents.cs @@ -7,9 +7,11 @@ using System.Reflection; +#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. + namespace Squidex.Domain.Apps.Events { - public static class SquidexEvents + public sealed class SquidexEvents { public static readonly Assembly Assembly = typeof(SquidexEvents).Assembly; } diff --git a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index 351faafbf..a314a3872 100644 --- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -14,10 +14,10 @@ - + - + diff --git a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs index 291fda481..e09a88879 100644 --- a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs +++ b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs @@ -110,14 +110,14 @@ namespace Squidex.Infrastructure.Assets } } - public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) { - return UploadCoreAsync(GetObjectName(id, version, suffix), stream, ct); + return UploadCoreAsync(GetObjectName(id, version, suffix), stream, overwrite, ct); } public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default) { - return UploadCoreAsync(fileName, stream, ct); + return UploadCoreAsync(fileName, stream, false, ct); } public Task DeleteAsync(string id, long version, string suffix) @@ -137,13 +137,13 @@ namespace Squidex.Infrastructure.Assets return blob.DeleteIfExistsAsync(); } - private async Task UploadCoreAsync(string blobName, Stream stream, CancellationToken ct = default) + private async Task UploadCoreAsync(string blobName, Stream stream, bool overwrite = false, CancellationToken ct = default) { try { var tempBlob = blobContainer.GetBlockBlobReference(blobName); - await tempBlob.UploadFromStreamAsync(stream, AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); + await tempBlob.UploadFromStreamAsync(stream, overwrite ? null : AccessCondition.GenerateIfNotExistsCondition(), null, null, ct); } catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 409) { diff --git a/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs b/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs new file mode 100644 index 000000000..09698e250 --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// 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 Microsoft.Azure.Documents.Client; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Squidex.Infrastructure.Diagnostics +{ + public sealed class CosmosDbHealthCheck : IHealthCheck + { + private readonly DocumentClient documentClient; + + public CosmosDbHealthCheck(Uri uri, string masterKey) + { + documentClient = new DocumentClient(uri, masterKey); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + await documentClient.ReadDatabaseFeedAsync(); + + return HealthCheckResult.Healthy("Application must query data from CosmosDB."); + } + } +} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs new file mode 100644 index 000000000..120f38704 --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + internal static class Constants + { + public const string Collection = "Events"; + + public const string LeaseCollection = "Leases"; + } +} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs new file mode 100644 index 000000000..c12c6548f --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class CosmosDbEvent + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("payload")] + public string Payload { get; set; } + + [JsonProperty("header")] + public EnvelopeHeaders Headers { get; set; } + + public static CosmosDbEvent FromEventData(EventData data) + { + return new CosmosDbEvent { Type = data.Type, Headers = data.Headers, Payload = data.Payload }; + } + + public EventData ToEventData() + { + return new EventData(Type, Headers, Payload); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs new file mode 100644 index 000000000..6a5dca9b3 --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class CosmosDbEventCommit + { + [JsonProperty("id")] + public Guid Id { get; set; } + + [JsonProperty("events")] + public CosmosDbEvent[] Events { get; set; } + + [JsonProperty("eventStreamOffset")] + public long EventStreamOffset { get; set; } + + [JsonProperty("eventsCount")] + public long EventsCount { get; set; } + + [JsonProperty("eventStream")] + public string EventStream { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs new file mode 100644 index 000000000..a07bf13ec --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.EventSourcing +{ + public sealed partial class CosmosDbEventStore : DisposableObjectBase, IEventStore, IInitializable + { + private readonly DocumentClient documentClient; + private readonly Uri collectionUri; + private readonly Uri databaseUri; + private readonly string masterKey; + private readonly string databaseId; + private readonly JsonSerializerSettings serializerSettings; + + public JsonSerializerSettings SerializerSettings + { + get { return serializerSettings; } + } + + public string DatabaseId + { + get { return databaseId; } + } + + public string MasterKey + { + get { return masterKey; } + } + + public Uri ServiceUri + { + get { return documentClient.ServiceEndpoint; } + } + + public CosmosDbEventStore(DocumentClient documentClient, string masterKey, string database, JsonSerializerSettings serializerSettings) + { + Guard.NotNull(documentClient, nameof(documentClient)); + Guard.NotNull(serializerSettings, nameof(serializerSettings)); + Guard.NotNullOrEmpty(masterKey, nameof(masterKey)); + Guard.NotNullOrEmpty(database, nameof(database)); + + this.documentClient = documentClient; + + databaseUri = UriFactory.CreateDatabaseUri(database); + databaseId = database; + + collectionUri = UriFactory.CreateDocumentCollectionUri(database, Constants.Collection); + + this.masterKey = masterKey; + + this.serializerSettings = serializerSettings; + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + documentClient.Dispose(); + } + } + + public async Task InitializeAsync(CancellationToken ct = default) + { + await documentClient.CreateDatabaseIfNotExistsAsync(new Database { Id = databaseId }); + + await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, + new DocumentCollection + { + Id = Constants.LeaseCollection, + }); + + await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, + new DocumentCollection + { + IndexingPolicy = new IndexingPolicy + { + IncludedPaths = new Collection + { + new IncludedPath + { + Path = "/*", + Indexes = new Collection + { + Index.Range(DataType.Number), + Index.Range(DataType.String), + } + } + } + }, + UniqueKeyPolicy = new UniqueKeyPolicy + { + UniqueKeys = new Collection + { + new UniqueKey + { + Paths = new Collection + { + $"/eventStream", + $"/eventStreamOffset" + } + } + } + }, + Id = Constants.Collection, + }, + new RequestOptions + { + PartitionKey = new PartitionKey($"/eventStream") + }); + } + } +} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs new file mode 100644 index 000000000..e0cccf559 --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// 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; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.EventSourcing +{ + public delegate bool EventPredicate(EventData data); + + public partial class CosmosDbEventStore : IEventStore, IInitializable + { + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) + { + Guard.NotNull(subscriber, nameof(subscriber)); + + ThrowIfDisposed(); + + return new CosmosDbSubscription(this, subscriber, streamFilter, position); + } + + public Task CreateIndexAsync(string property) + { + Guard.NotNullOrEmpty(property, nameof(property)); + + ThrowIfDisposed(); + + return Task.CompletedTask; + } + + public async Task> QueryAsync(string streamName, long streamPosition = 0) + { + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + + ThrowIfDisposed(); + + using (Profiler.TraceMethod()) + { + var query = FilterBuilder.ByStreamName(streamName, streamPosition - MaxCommitSize); + + var result = new List(); + + await documentClient.QueryAsync(collectionUri, query, commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (eventStreamOffset >= streamPosition) + { + var eventData = @event.ToEventData(); + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); + } + } + + return TaskHelper.Done; + }); + + return result; + } + } + + public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) + { + Guard.NotNull(callback, nameof(callback)); + Guard.NotNullOrEmpty(property, nameof(property)); + Guard.NotNull(value, nameof(value)); + + ThrowIfDisposed(); + + StreamPosition lastPosition = position; + + var filterDefinition = FilterBuilder.CreateByProperty(property, value, lastPosition); + var filterExpression = FilterBuilder.CreateExpression(property, value); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) + { + Guard.NotNull(callback, nameof(callback)); + + ThrowIfDisposed(); + + StreamPosition lastPosition = position; + + var filterDefinition = FilterBuilder.CreateByFilter(streamFilter, lastPosition); + var filterExpression = FilterBuilder.CreateExpression(null, null); + + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); + } + + private async Task QueryAsync(Func callback, StreamPosition lastPosition, SqlQuerySpec query, EventPredicate filterExpression, CancellationToken ct = default) + { + using (Profiler.TraceMethod()) + { + await documentClient.QueryAsync(collectionUri, query, async commit => + { + var eventStreamOffset = (int)commit.EventStreamOffset; + + var commitTimestamp = commit.Timestamp; + var commitOffset = 0; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) + { + var eventData = @event.ToEventData(); + + if (filterExpression(eventData)) + { + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + + await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); + } + } + + commitOffset++; + } + }, ct); + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs new file mode 100644 index 000000000..45144e56e --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs @@ -0,0 +1,149 @@ +// ========================================================================== +// 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.Net; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using NodaTime; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.EventSourcing +{ + public partial class CosmosDbEventStore + { + private const int MaxWriteAttempts = 20; + private const int MaxCommitSize = 10; + + public Task DeleteStreamAsync(string streamName) + { + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + + ThrowIfDisposed(); + + var query = FilterBuilder.AllIds(streamName); + + return documentClient.QueryAsync(collectionUri, query, commit => + { + var documentUri = UriFactory.CreateDocumentUri(databaseId, Constants.Collection, commit.Id.ToString()); + + return documentClient.DeleteDocumentAsync(documentUri); + }); + } + + public Task AppendAsync(Guid commitId, string streamName, ICollection events) + { + return AppendAsync(commitId, streamName, EtagVersion.Any, events); + } + + public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + Guard.NotEmpty(commitId, nameof(commitId)); + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + Guard.NotNull(events, nameof(events)); + Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); + + ThrowIfDisposed(); + + using (Profiler.TraceMethod()) + { + if (events.Count == 0) + { + return; + } + + var currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); + + for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) + { + try + { + await documentClient.CreateDocumentAsync(collectionUri, commit); + + return; + } + catch (DocumentClientException ex) + { + if (ex.StatusCode == HttpStatusCode.Conflict) + { + currentVersion = await GetEventStreamOffsetAsync(streamName); + + if (expectedVersion != EtagVersion.Any) + { + throw new WrongEventVersionException(currentVersion, expectedVersion); + } + + if (attempt < MaxWriteAttempts) + { + expectedVersion = currentVersion; + } + else + { + throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); + } + } + else + { + throw; + } + } + } + } + } + + private async Task GetEventStreamOffsetAsync(string streamName) + { + var query = + documentClient.CreateDocumentQuery(collectionUri, + FilterBuilder.LastPosition(streamName)); + + var document = await query.FirstOrDefaultAsync(); + + if (document != null) + { + return document.EventStreamOffset + document.EventsCount; + } + + return EtagVersion.Empty; + } + + private static CosmosDbEventCommit BuildCommit(Guid commitId, string streamName, long expectedVersion, ICollection events) + { + var commitEvents = new CosmosDbEvent[events.Count]; + + var i = 0; + + foreach (var e in events) + { + var mongoEvent = CosmosDbEvent.FromEventData(e); + + commitEvents[i++] = mongoEvent; + } + + var mongoCommit = new CosmosDbEventCommit + { + Id = commitId, + Events = commitEvents, + EventsCount = events.Count, + EventStream = streamName, + EventStreamOffset = expectedVersion, + Timestamp = SystemClock.Instance.GetCurrentInstant().ToUnixTimeTicks() + }; + + return mongoCommit; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs new file mode 100644 index 000000000..fa5d8af86 --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs @@ -0,0 +1,150 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.ChangeFeedProcessor.FeedProcessing; +using Newtonsoft.Json; +using Builder = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorBuilder; +using Collection = Microsoft.Azure.Documents.ChangeFeedProcessor.DocumentCollectionInfo; +using Options = Microsoft.Azure.Documents.ChangeFeedProcessor.ChangeFeedProcessorOptions; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class CosmosDbSubscription : IEventSubscription, IChangeFeedObserverFactory, IChangeFeedObserver + { + private readonly TaskCompletionSource processorStopRequested = new TaskCompletionSource(); + private readonly Task processorTask; + private readonly CosmosDbEventStore store; + private readonly Regex regex; + private readonly string hostName; + private readonly IEventSubscriber subscriber; + + public CosmosDbSubscription(CosmosDbEventStore store, IEventSubscriber subscriber, string streamFilter, string position = null) + { + this.store = store; + + var fromBeginning = string.IsNullOrWhiteSpace(position); + + if (fromBeginning) + { + hostName = $"squidex.{DateTime.UtcNow.Ticks.ToString()}"; + } + else + { + hostName = position; + } + + if (!StreamFilter.IsAll(streamFilter)) + { + regex = new Regex(streamFilter); + } + + this.subscriber = subscriber; + + processorTask = Task.Run(async () => + { + try + { + Collection CreateCollection(string name) + { + var collection = new Collection(); + + collection.CollectionName = name; + collection.DatabaseName = store.DatabaseId; + collection.MasterKey = store.MasterKey; + collection.Uri = store.ServiceUri; + + return collection; + } + + var processor = + await new Builder() + .WithFeedCollection(CreateCollection(Constants.Collection)) + .WithLeaseCollection(CreateCollection(Constants.LeaseCollection)) + .WithHostName(hostName) + .WithProcessorOptions(new Options { StartFromBeginning = fromBeginning, LeasePrefix = hostName }) + .WithObserverFactory(this) + .BuildAsync(); + + await processor.StartAsync(); + await processorStopRequested.Task; + await processor.StopAsync(); + } + catch (Exception ex) + { + await subscriber.OnErrorAsync(this, ex); + } + }); + } + + public IChangeFeedObserver CreateObserver() + { + return this; + } + + public async Task CloseAsync(IChangeFeedObserverContext context, ChangeFeedObserverCloseReason reason) + { + if (reason == ChangeFeedObserverCloseReason.ObserverError) + { + await subscriber.OnErrorAsync(this, new InvalidOperationException("Change feed observer failed.")); + } + } + + public Task OpenAsync(IChangeFeedObserverContext context) + { + return Task.CompletedTask; + } + + public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, CancellationToken cancellationToken) + { + if (!processorStopRequested.Task.IsCompleted) + { + foreach (var document in docs) + { + if (!processorStopRequested.Task.IsCompleted) + { + var streamName = document.GetPropertyValue("eventStream"); + + if (regex == null || regex.IsMatch(streamName)) + { + var commit = JsonConvert.DeserializeObject(document.ToString(), store.SerializerSettings); + + var eventStreamOffset = (int)commit.EventStreamOffset; + + foreach (var @event in commit.Events) + { + eventStreamOffset++; + + var eventData = @event.ToEventData(); + + await subscriber.OnEventAsync(this, new StoredEvent(commit.EventStream, hostName, eventStreamOffset, eventData)); + } + } + } + } + } + } + + public void WakeUp() + { + } + + public Task StopAsync() + { + processorStopRequested.SetResult(true); + + return processorTask; + } + } +} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs new file mode 100644 index 000000000..b6bd7686c --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs @@ -0,0 +1,156 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.Azure.Documents; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal static class FilterBuilder + { + public static SqlQuerySpec AllIds(string streamName) + { + var query = + $"SELECT TOP 1 " + + $" e.id," + + $" e.eventsCount " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"ORDER BY e.eventStreamOffset DESC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec LastPosition(string streamName) + { + var query = + $"SELECT TOP 1 " + + $" e.eventStreamOffset," + + $" e.eventsCount " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"ORDER BY e.eventStreamOffset DESC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec ByStreamName(string streamName, long streamPosition = 0) + { + var query = + $"SELECT * " + + $"FROM {Constants.Collection} e " + + $"WHERE " + + $" e.eventStream = @name " + + $"AND e.eventStreamOffset >= @position " + + $"ORDER BY e.eventStreamOffset ASC"; + + var parameters = new SqlParameterCollection + { + new SqlParameter("@name", streamName), + new SqlParameter("@position", streamPosition) + }; + + return new SqlQuerySpec(query, parameters); + } + + public static SqlQuerySpec CreateByProperty(string property, object value, StreamPosition streamPosition) + { + var filters = new List(); + + var parameters = new SqlParameterCollection(); + + filters.ForPosition(parameters, streamPosition); + filters.ForProperty(parameters, property, value); + + return BuildQuery(filters, parameters); + } + + public static SqlQuerySpec CreateByFilter(string streamFilter, StreamPosition streamPosition) + { + var filters = new List(); + + var parameters = new SqlParameterCollection(); + + filters.ForPosition(parameters, streamPosition); + filters.ForRegex(parameters, streamFilter); + + return BuildQuery(filters, parameters); + } + + private static SqlQuerySpec BuildQuery(List filters, SqlParameterCollection parameters) + { + var query = $"SELECT * FROM {Constants.Collection} e WHERE {string.Join(" AND ", filters)} ORDER BY e.timestamp"; + + return new SqlQuerySpec(query, parameters); + } + + private static void ForProperty(this List filters, SqlParameterCollection parameters, string property, object value) + { + filters.Add($"ARRAY_CONTAINS(e.events, {{ \"header\": {{ \"{property}\": @value }} }}, true)"); + + parameters.Add(new SqlParameter("@value", value)); + } + + private static void ForRegex(this List filters, SqlParameterCollection parameters, string streamFilter) + { + if (!StreamFilter.IsAll(streamFilter)) + { + if (streamFilter.Contains("^")) + { + filters.Add($"STARTSWITH(e.eventStream, @filter)"); + } + else + { + filters.Add($"e.eventStream = @filter"); + } + + parameters.Add(new SqlParameter("@filter", streamFilter)); + } + } + + private static void ForPosition(this List filters, SqlParameterCollection parameters, StreamPosition streamPosition) + { + if (streamPosition.IsEndOfCommit) + { + filters.Add($"e.timestamp > @time"); + } + else + { + filters.Add($"e.timestamp >= @time"); + } + + parameters.Add(new SqlParameter("@time", streamPosition.Timestamp)); + } + + public static EventPredicate CreateExpression(string property, object value) + { + if (!string.IsNullOrWhiteSpace(property)) + { + var jsonValue = JsonValue.Create(value); + + return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); + } + else + { + return x => true; + } + } + } +} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs new file mode 100644 index 000000000..c24e93ff1 --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using Microsoft.Azure.Documents.Linq; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal static class FilterExtensions + { + public static async Task FirstOrDefaultAsync(this IQueryable queryable, CancellationToken ct = default) + { + var documentQuery = queryable.AsDocumentQuery(); + + using (documentQuery) + { + if (documentQuery.HasMoreResults) + { + var results = await documentQuery.ExecuteNextAsync(ct); + + return results.FirstOrDefault(); + } + } + + return default; + } + + public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default) + { + var query = documentClient.CreateDocumentQuery(collectionUri, querySpec); + + return query.QueryAsync(handler, ct); + } + + public static async Task QueryAsync(this IQueryable queryable, Func handler, CancellationToken ct = default) + { + var documentQuery = queryable.AsDocumentQuery(); + + using (documentQuery) + { + while (documentQuery.HasMoreResults && !ct.IsCancellationRequested) + { + var items = await documentQuery.ExecuteNextAsync(ct); + + foreach (var item in items) + { + await handler(item); + } + } + } + } + } +} diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs new file mode 100644 index 000000000..f0626ee5d --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/StreamPosition.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing +{ + internal sealed class StreamPosition + { + public long Timestamp { get; } + + public long CommitOffset { get; } + + public long CommitSize { get; } + + public bool IsEndOfCommit + { + get { return CommitOffset == CommitSize - 1; } + } + + public StreamPosition(long timestamp, long commitOffset, long commitSize) + { + Timestamp = timestamp; + + CommitOffset = commitOffset; + CommitSize = commitSize; + } + + public static implicit operator string(StreamPosition position) + { + var parts = new object[] + { + position.Timestamp, + position.CommitOffset, + position.CommitSize + }; + + return string.Join("-", parts); + } + + public static implicit operator StreamPosition(string position) + { + if (!string.IsNullOrWhiteSpace(position)) + { + var parts = position.Split('-'); + + return new StreamPosition(long.Parse(parts[0]), long.Parse(parts[1]), long.Parse(parts[2])); + } + + return new StreamPosition(0, -1, -1); + } + } +} diff --git a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj index 755262980..5351ea162 100644 --- a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj +++ b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj @@ -5,6 +5,8 @@ 7.3 + + diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs index 8a2dd111f..cc05285d8 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs @@ -6,6 +6,8 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using EventStore.ClientAPI; using Squidex.Infrastructure.Json; @@ -15,24 +17,54 @@ namespace Squidex.Infrastructure.EventSourcing { public static class Formatter { - public static StoredEvent Read(ResolvedEvent resolvedEvent, IJsonSerializer serializer) + private static readonly HashSet PrivateHeaders = new HashSet { "$v", "$p", "$c", "$causedBy" }; + + public static StoredEvent Read(ResolvedEvent resolvedEvent, string prefix, IJsonSerializer serializer) { var @event = resolvedEvent.Event; - var metadata = Encoding.UTF8.GetString(@event.Data); + var eventPayload = Encoding.UTF8.GetString(@event.Data); + var eventHeaders = GetHeaders(serializer, @event); - var headersJson = Encoding.UTF8.GetString(@event.Metadata); - var headers = serializer.Deserialize(headersJson); + var eventData = new EventData(@event.EventType, eventHeaders, eventPayload); - var eventData = new EventData(@event.EventType, headers, metadata); + var streamName = GetStreamName(prefix, @event); return new StoredEvent( - @event.EventStreamId, + streamName, resolvedEvent.OriginalEventNumber.ToString(), resolvedEvent.Event.EventNumber, eventData); } + private static string GetStreamName(string prefix, RecordedEvent @event) + { + var streamName = @event.EventStreamId; + + if (streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + streamName = streamName.Substring(prefix.Length + 1); + } + + return streamName; + } + + private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, RecordedEvent @event) + { + var headersJson = Encoding.UTF8.GetString(@event.Metadata); + var headers = serializer.Deserialize(headersJson); + + foreach (var key in headers.Keys.ToList()) + { + if (PrivateHeaders.Contains(key)) + { + headers.Remove(key); + } + } + + return headers; + } + public static EventStoreData Write(EventData eventData, IJsonSerializer serializer) { var payload = Encoding.UTF8.GetBytes(eventData.Payload); diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs index 83f81fb92..d06c1ea15 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using EventStore.ClientAPI; +using EventStore.ClientAPI.Exceptions; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Log; @@ -52,18 +53,26 @@ namespace Squidex.Infrastructure.EventSourcing await projectionClient.ConnectAsync(ct); } - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null) + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) { - return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, streamFilter); + Guard.NotNull(streamFilter, nameof(streamFilter)); + + return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, prefix, streamFilter); } public Task CreateIndexAsync(string property) { + Guard.NotNullOrEmpty(property, nameof(property)); + return projectionClient.CreateProjectionAsync(property, string.Empty); } public async Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) { + Guard.NotNull(callback, nameof(callback)); + Guard.NotNullOrEmpty(property, nameof(property)); + Guard.NotNull(value, nameof(value)); + using (Profiler.TraceMethod()) { var streamName = await projectionClient.CreateProjectionAsync(property, value); @@ -76,6 +85,8 @@ namespace Squidex.Infrastructure.EventSourcing public async Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) { + Guard.NotNull(callback, nameof(callback)); + using (Profiler.TraceMethod()) { var streamName = await projectionClient.CreateProjectionAsync(streamFilter); @@ -91,7 +102,7 @@ namespace Squidex.Infrastructure.EventSourcing StreamEventsSlice currentSlice; do { - currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false); + currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true); if (currentSlice.Status == SliceReadStatus.Success) { @@ -99,7 +110,7 @@ namespace Squidex.Infrastructure.EventSourcing foreach (var resolved in currentSlice.Events) { - var storedEvent = Formatter.Read(resolved, serializer); + var storedEvent = Formatter.Read(resolved, prefix, serializer); await callback(storedEvent); } @@ -110,16 +121,18 @@ namespace Squidex.Infrastructure.EventSourcing public async Task> QueryAsync(string streamName, long streamPosition = 0) { + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + using (Profiler.TraceMethod()) { var result = new List(); - var sliceStart = streamPosition; + var sliceStart = streamPosition >= 0 ? streamPosition : StreamPosition.Start; StreamEventsSlice currentSlice; do { - currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false); + currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, true); if (currentSlice.Status == SliceReadStatus.Success) { @@ -127,7 +140,7 @@ namespace Squidex.Infrastructure.EventSourcing foreach (var resolved in currentSlice.Events) { - var storedEvent = Formatter.Read(resolved, serializer); + var storedEvent = Formatter.Read(resolved, prefix, serializer); result.Add(storedEvent); } @@ -141,7 +154,9 @@ namespace Squidex.Infrastructure.EventSourcing public Task DeleteStreamAsync(string streamName) { - return connection.DeleteStreamAsync(streamName, ExpectedVersion.Any); + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + + return connection.DeleteStreamAsync(GetStreamName(streamName), ExpectedVersion.Any); } public Task AppendAsync(Guid commitId, string streamName, ICollection events) @@ -158,40 +173,47 @@ namespace Squidex.Infrastructure.EventSourcing private async Task AppendEventsInternalAsync(string streamName, long expectedVersion, ICollection events) { + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + Guard.NotNull(events, nameof(events)); + using (Profiler.TraceMethod(nameof(AppendAsync))) { - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - if (events.Count == 0) { return; } - var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList(); - - if (eventsToSave.Count < WritePageSize) + try { - await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave); - } - else - { - using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion)) + var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList(); + + if (eventsToSave.Count < WritePageSize) + { + await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave); + } + else { - for (var p = 0; p < eventsToSave.Count; p += WritePageSize) + using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion)) { - await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize)); - } + for (var p = 0; p < eventsToSave.Count; p += WritePageSize) + { + await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize)); + } - await transaction.CommitAsync(); + await transaction.CommitAsync(); + } } } + catch (WrongExpectedVersionException ex) + { + throw new WrongEventVersionException(ParseVersion(ex.Message), expectedVersion); + } } } - public Task DeleteManyAsync(string property, object value) + private static int ParseVersion(string message) { - throw new NotSupportedException(); + return int.Parse(message.Substring(message.LastIndexOf(':') + 1)); } private string GetStreamName(string streamName) diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs index a1d3faf01..0f06e4e77 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -18,6 +18,7 @@ namespace Squidex.Infrastructure.EventSourcing private readonly IEventStoreConnection connection; private readonly IEventSubscriber subscriber; private readonly IJsonSerializer serializer; + private readonly string prefix; private readonly EventStoreCatchUpSubscription subscription; private readonly long? position; @@ -27,13 +28,13 @@ namespace Squidex.Infrastructure.EventSourcing IJsonSerializer serializer, ProjectionClient projectionClient, string position, + string prefix, string streamFilter) { - Guard.NotNull(subscriber, nameof(subscriber)); - this.connection = connection; this.position = projectionClient.ParsePositionOrNull(position); + this.prefix = prefix; var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result; @@ -61,7 +62,7 @@ namespace Squidex.Infrastructure.EventSourcing return connection.SubscribeToStreamFrom(streamName, position, settings, (s, e) => { - var storedEvent = Formatter.Read(e, serializer); + var storedEvent = Formatter.Read(e, prefix, serializer); subscriber.OnEventAsync(this, storedEvent).Wait(); }, null, diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs index 2873e8625..ca098bb1e 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs @@ -137,7 +137,7 @@ namespace Squidex.Infrastructure.EventSourcing public long ParsePosition(string position) { - return long.TryParse(position, out var parsedPosition) ? parsedPosition : 0; + return long.TryParse(position, out var parsedPosition) ? parsedPosition + 1 : StreamPosition.Start; } } } diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs index 7cb74737a..f76589938 100644 --- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs @@ -7,7 +7,6 @@ using System; using System.IO; -using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -81,14 +80,14 @@ namespace Squidex.Infrastructure.Assets } } - public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) { - return UploadCoreAsync(GetObjectName(id, version, suffix), stream, ct); + return UploadCoreAsync(GetObjectName(id, version, suffix), stream, overwrite, ct); } public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default) { - return UploadCoreAsync(fileName, stream, ct); + return UploadCoreAsync(fileName, stream, false, ct); } public Task DeleteAsync(string id, long version, string suffix) @@ -101,11 +100,11 @@ namespace Squidex.Infrastructure.Assets return DeleteCoreAsync(fileName); } - private async Task UploadCoreAsync(string objectName, Stream stream, CancellationToken ct = default) + private async Task UploadCoreAsync(string objectName, Stream stream, bool overwrite = false, CancellationToken ct = default) { try { - await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream, IfNotExists, ct); + await storageClient.UploadObjectAsync(bucketName, objectName, "application/octet-stream", stream, overwrite ? null : IfNotExists, ct); } catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) { @@ -141,7 +140,7 @@ namespace Squidex.Infrastructure.Assets private static string GetFileName(string id, long version, string suffix) { - return string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x))); + return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); } } } diff --git a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs b/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs index e373715a2..997fd9068 100644 --- a/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/Assets/MongoGridFsAssetStore.cs @@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.Assets using (var readStream = await bucket.OpenDownloadStreamAsync(sourceFileName, cancellationToken: ct)) { - await UploadFileCoreAsync(target, readStream, ct); + await UploadFileCoreAsync(target, readStream, false, ct); } } catch (GridFSFileNotFoundException ex) @@ -78,14 +78,14 @@ namespace Squidex.Infrastructure.Assets } } - public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) { - return UploadFileCoreAsync(GetFileName(id, version, suffix), stream, ct); + return UploadFileCoreAsync(GetFileName(id, version, suffix), stream, overwrite, ct); } public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default) { - return UploadFileCoreAsync(fileName, stream, ct); + return UploadFileCoreAsync(fileName, stream, false, ct); } public Task DeleteAsync(string id, long version, string suffix) @@ -110,10 +110,15 @@ namespace Squidex.Infrastructure.Assets } } - private async Task UploadFileCoreAsync(string id, Stream stream, CancellationToken ct = default) + private async Task UploadFileCoreAsync(string id, Stream stream, bool overwrite = false, CancellationToken ct = default) { try { + if (overwrite) + { + await bucket.DeleteAsync(id, ct); + } + await bucket.UploadFromStreamAsync(id, id, stream, cancellationToken: ct); } catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) @@ -128,7 +133,7 @@ namespace Squidex.Infrastructure.Assets private static string GetFileName(string id, long version, string suffix) { - return string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x))); + return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs index b9bdcbf10..4f254ddd4 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -10,36 +10,43 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; +using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.MongoDb; +using EventFilter = MongoDB.Driver.FilterDefinition; namespace Squidex.Infrastructure.EventSourcing { + public delegate bool EventPredicate(EventData data); + public partial class MongoEventStore : MongoRepositoryBase, IEventStore { public Task CreateIndexAsync(string property) { + Guard.NotNullOrEmpty(property, nameof(property)); + return Collection.Indexes.CreateOneAsync( new CreateIndexModel(Index.Ascending(CreateIndexPath(property)))); } - public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null) + public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null) { Guard.NotNull(subscriber, nameof(subscriber)); - Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter)); return new PollingSubscription(this, subscriber, streamFilter, position); } public async Task> QueryAsync(string streamName, long streamPosition = 0) { + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + using (Profiler.TraceMethod()) { var commits = await Collection.Find( Filter.And( Filter.Eq(EventStreamField, streamName), - Filter.Gte(EventStreamOffsetField, streamPosition - 1))) + Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize))) .Sort(Sort.Ascending(TimestampField)).ToListAsync(); var result = new List(); @@ -51,13 +58,13 @@ namespace Squidex.Infrastructure.EventSourcing var commitTimestamp = commit.Timestamp; var commitOffset = 0; - foreach (var e in commit.Events) + foreach (var @event in commit.Events) { eventStreamOffset++; if (eventStreamOffset >= streamPosition) { - var eventData = e.ToEventData(); + var eventData = @event.ToEventData(); var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); result.Add(new StoredEvent(streamName, eventToken, eventStreamOffset, eventData)); @@ -72,12 +79,15 @@ namespace Squidex.Infrastructure.EventSourcing public Task QueryAsync(Func callback, string property, object value, string position = null, CancellationToken ct = default) { Guard.NotNull(callback, nameof(callback)); + Guard.NotNullOrEmpty(property, nameof(property)); + Guard.NotNull(value, nameof(value)); StreamPosition lastPosition = position; - var filter = CreateFilter(property, value, lastPosition); + var filterDefinition = CreateFilter(property, value, lastPosition); + var filterExpression = CreateFilterExpression(property, value); - return QueryAsync(callback, lastPosition, filter, ct); + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); } public Task QueryAsync(Func callback, string streamFilter = null, string position = null, CancellationToken ct = default) @@ -86,68 +96,73 @@ namespace Squidex.Infrastructure.EventSourcing StreamPosition lastPosition = position; - var filter = CreateFilter(streamFilter, lastPosition); + var filterDefinition = CreateFilter(streamFilter, lastPosition); + var filterExpression = CreateFilterExpression(null, null); - return QueryAsync(callback, lastPosition, filter, ct); + return QueryAsync(callback, lastPosition, filterDefinition, filterExpression, ct); } - private async Task QueryAsync(Func callback, StreamPosition lastPosition, FilterDefinition filter, CancellationToken ct = default) + private async Task QueryAsync(Func callback, StreamPosition lastPosition, EventFilter filterDefinition, EventPredicate filterExpression, CancellationToken ct = default) { using (Profiler.TraceMethod()) { - await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)).ForEachPipelineAsync(async commit => + await Collection.Find(filterDefinition).Sort(Sort.Ascending(TimestampField)).ForEachPipelineAsync(async commit => { var eventStreamOffset = (int)commit.EventStreamOffset; var commitTimestamp = commit.Timestamp; var commitOffset = 0; - foreach (var e in commit.Events) + foreach (var @event in commit.Events) { eventStreamOffset++; if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) { - var eventData = e.ToEventData(); - var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); + var eventData = @event.ToEventData(); - await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); + if (filterExpression(eventData)) + { + var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); - commitOffset++; + await callback(new StoredEvent(commit.EventStream, eventToken, eventStreamOffset, eventData)); + } } + + commitOffset++; } }, ct); } } - private static FilterDefinition CreateFilter(string property, object value, StreamPosition streamPosition) + private static EventFilter CreateFilter(string property, object value, StreamPosition streamPosition) { - var filters = new List>(); + var filters = new List(); - FilterByPosition(streamPosition, filters); - FilterByProperty(property, value, filters); + AppendByPosition(streamPosition, filters); + AppendByProperty(property, value, filters); return Filter.And(filters); } - private static FilterDefinition CreateFilter(string streamFilter, StreamPosition streamPosition) + private static EventFilter CreateFilter(string streamFilter, StreamPosition streamPosition) { - var filters = new List>(); + var filters = new List(); - FilterByPosition(streamPosition, filters); - FilterByStream(streamFilter, filters); + AppendByPosition(streamPosition, filters); + AppendByStream(streamFilter, filters); return Filter.And(filters); } - private static void FilterByProperty(string property, object value, List> filters) + private static void AppendByProperty(string property, object value, List filters) { filters.Add(Filter.Eq(CreateIndexPath(property), value)); } - private static void FilterByStream(string streamFilter, List> filters) + private static void AppendByStream(string streamFilter, List filters) { - if (!string.IsNullOrWhiteSpace(streamFilter) && !string.Equals(streamFilter, ".*", StringComparison.OrdinalIgnoreCase)) + if (!StreamFilter.IsAll(streamFilter)) { if (streamFilter.Contains("^")) { @@ -160,7 +175,7 @@ namespace Squidex.Infrastructure.EventSourcing } } - private static void FilterByPosition(StreamPosition streamPosition, List> filters) + private static void AppendByPosition(StreamPosition streamPosition, List filters) { if (streamPosition.IsEndOfCommit) { @@ -172,6 +187,20 @@ namespace Squidex.Infrastructure.EventSourcing } } + private static EventPredicate CreateFilterExpression(string property, object value) + { + if (!string.IsNullOrWhiteSpace(property)) + { + var jsonValue = JsonValue.Create(value); + + return x => x.Headers.TryGetValue(property, out var p) && p.Equals(jsonValue); + } + else + { + return x => true; + } + } + private static string CreateIndexPath(string property) { return $"Events.Metadata.{property}"; diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs index 4e77f4991..29a9de831 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -16,17 +16,15 @@ namespace Squidex.Infrastructure.EventSourcing { public partial class MongoEventStore { + private const int MaxCommitSize = 10; private const int MaxWriteAttempts = 20; private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); public Task DeleteStreamAsync(string streamName) { - return Collection.DeleteManyAsync(x => x.EventStream == streamName); - } + Guard.NotNullOrEmpty(streamName, nameof(streamName)); - public Task DeleteManyAsync(string property, object value) - { - return Collection.DeleteManyAsync(Filter.Eq(CreateIndexPath(property), value)); + return Collection.DeleteManyAsync(x => x.EventStream == streamName); } public Task AppendAsync(Guid commitId, string streamName, ICollection events) @@ -36,18 +34,22 @@ namespace Squidex.Infrastructure.EventSourcing public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) { + Guard.NotEmpty(commitId, nameof(commitId)); + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + Guard.NotNull(events, nameof(events)); + Guard.LessThan(events.Count, MaxCommitSize, "events.Count"); + Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + Guard.NotNull(events, nameof(events)); + using (Profiler.TraceMethod()) { - Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); - Guard.NotNullOrEmpty(streamName, nameof(streamName)); - Guard.NotNull(events, nameof(events)); - if (events.Count == 0) { return; } - var currentVersion = await GetEventStreamOffset(streamName); + var currentVersion = await GetEventStreamOffsetAsync(streamName); if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion) { @@ -70,7 +72,7 @@ namespace Squidex.Infrastructure.EventSourcing { if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { - currentVersion = await GetEventStreamOffset(streamName); + currentVersion = await GetEventStreamOffsetAsync(streamName); if (expectedVersion != EtagVersion.Any) { @@ -95,7 +97,7 @@ namespace Squidex.Infrastructure.EventSourcing } } - private async Task GetEventStreamOffset(string streamName) + private async Task GetEventStreamOffsetAsync(string streamName) { var document = await Collection.Find(Filter.Eq(EventStreamField, streamName)) diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs index 45c0910e9..661c83435 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs @@ -9,7 +9,7 @@ using MongoDB.Bson; namespace Squidex.Infrastructure.EventSourcing { - public sealed class StreamPosition + internal sealed class StreamPosition { private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index 77019a716..7fe870e3c 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -8,6 +8,7 @@ using System; using System.Linq; using System.Reflection; +using System.Threading; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using Newtonsoft.Json; @@ -17,36 +18,41 @@ namespace Squidex.Infrastructure.MongoDb { public static class BsonJsonConvention { + private static volatile int isRegistered; + public static void Register(JsonSerializer serializer) { - var pack = new ConventionPack(); - - pack.AddMemberMapConvention("JsonBson", memberMap => + if (Interlocked.Increment(ref isRegistered) == 1) { - var attributes = memberMap.MemberInfo.GetCustomAttributes(); + var pack = new ConventionPack(); - if (attributes.OfType().Any()) + pack.AddMemberMapConvention("JsonBson", memberMap => { - var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); - var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); + var attributes = memberMap.MemberInfo.GetCustomAttributes(); - memberMap.SetSerializer((IBsonSerializer)bsonSerializer); - } - else if (memberMap.MemberType == typeof(JToken)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - else if (memberMap.MemberType == typeof(JObject)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - else if (memberMap.MemberType == typeof(JValue)) - { - memberMap.SetSerializer(JTokenSerializer.Instance); - } - }); + if (attributes.OfType().Any()) + { + var bsonSerializerType = typeof(BsonJsonSerializer<>).MakeGenericType(memberMap.MemberType); + var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); + + memberMap.SetSerializer((IBsonSerializer)bsonSerializer); + } + else if (memberMap.MemberType == typeof(JToken)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + else if (memberMap.MemberType == typeof(JObject)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + else if (memberMap.MemberType == typeof(JValue)) + { + memberMap.SetSerializer(JTokenSerializer.Instance); + } + }); - ConventionRegistry.Register("json", pack, t => true); + ConventionRegistry.Register("json", pack, t => true); + } } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs index b4a5d5926..6979cafe3 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/InstantSerializer.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; +using System.Threading; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using NodaTime; @@ -14,16 +14,14 @@ namespace Squidex.Infrastructure.MongoDb { public sealed class InstantSerializer : SerializerBase, IBsonPolymorphicSerializer { - private static readonly Lazy Registerer = new Lazy(() => - { - BsonSerializer.RegisterSerializer(new InstantSerializer()); - - return true; - }); + private static volatile int isRegistered; - public static bool Register() + public static void Register() { - return !Registerer.IsValueCreated && Registerer.Value; + if (Interlocked.Increment(ref isRegistered) == 1) + { + BsonSerializer.RegisterSerializer(new InstantSerializer()); + } } public bool IsDiscriminatorCompatibleWithObjectSerializer diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs new file mode 100644 index 000000000..65462dc6c --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class MongoDbOptions + { + public bool IsCosmosDb { get; set; } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index 631727d92..ec0a29528 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -22,6 +22,16 @@ namespace Squidex.Infrastructure.MongoDb { private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName) + { + var options = new ListCollectionNamesOptions + { + Filter = new BsonDocument("name", collectionName) + }; + + return (await database.ListCollectionNamesAsync(options)).Any(); + } + public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document) { try diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 00237ee83..689bdfe8e 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -44,6 +44,7 @@ namespace Squidex.Infrastructure.MongoDb static MongoRepositoryBase() { RefTokenSerializer.Register(); + InstantSerializer.Register(); } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs index 6f8761148..b4c45f945 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; +using System.Threading; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; @@ -13,16 +13,14 @@ namespace Squidex.Infrastructure.MongoDb { public class RefTokenSerializer : ClassSerializerBase { - private static readonly Lazy Registerer = new Lazy(() => - { - BsonSerializer.RegisterSerializer(new RefTokenSerializer()); - - return true; - }); + private static volatile int isRegistered; - public static bool Register() + public static void Register() { - return !Registerer.IsValueCreated && Registerer.Value; + if (Interlocked.Increment(ref isRegistered) == 1) + { + BsonSerializer.RegisterSerializer(new RefTokenSerializer()); + } } protected override RefToken DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index 683baf450..0acd60699 100644 --- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 3f0a356b1..61afdd1cf 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -5,10 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using Newtonsoft.Json; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.MongoDb; @@ -16,9 +19,12 @@ namespace Squidex.Infrastructure.States { public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore { - public MongoSnapshotStore(IMongoDatabase database) + public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) : base(database) { + Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); + + BsonJsonConvention.Register(jsonSerializer); } protected override string CollectionName() @@ -55,11 +61,11 @@ namespace Squidex.Infrastructure.States } } - public async Task ReadAllAsync(System.Func callback) + public async Task ReadAllAsync(Func callback, CancellationToken ct = default) { using (Profiler.TraceMethod>()) { - await Collection.Find(new BsonDocument()).ForEachAsync(x => callback(x.Doc, x.Version)); + await Collection.Find(new BsonDocument()).ForEachPipelineAsync(x => callback(x.Doc, x.Version), ct); } } diff --git a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs index 18a544342..21d44ae47 100644 --- a/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs +++ b/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs @@ -49,8 +49,8 @@ namespace Squidex.Infrastructure.CQRS.Events this.exchange = exchange; this.eventsFilter = eventsFilter; - this.jsonSerializer = jsonSerializer; this.eventPublisherName = eventPublisherName; + this.jsonSerializer = jsonSerializer; } protected override void DisposeObject(bool disposing) diff --git a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs b/src/Squidex.Infrastructure.Redis/RedisPubSub.cs index 3feaf1510..74ef5c100 100644 --- a/src/Squidex.Infrastructure.Redis/RedisPubSub.cs +++ b/src/Squidex.Infrastructure.Redis/RedisPubSub.cs @@ -19,22 +19,19 @@ namespace Squidex.Infrastructure public sealed class RedisPubSub : IPubSub, IInitializable { private readonly ConcurrentDictionary subscriptions = new ConcurrentDictionary(); - private readonly Lazy redisClient; + private readonly IConnectionMultiplexer redis; private readonly IJsonSerializer serializer; - private readonly Lazy redisSubscriber; private readonly ISemanticLog log; + private ISubscriber redisSubscriber; - public RedisPubSub(Lazy redis, IJsonSerializer serializer, ISemanticLog log) + public RedisPubSub(IConnectionMultiplexer redis, IJsonSerializer serializer, ISemanticLog log) { Guard.NotNull(serializer, nameof(serializer)); Guard.NotNull(redis, nameof(redis)); Guard.NotNull(log, nameof(log)); this.log = log; - - redisClient = redis; - redisSubscriber = new Lazy(() => redis.Value.GetSubscriber()); - + this.redis = redis; this.serializer = serializer; } @@ -42,13 +39,15 @@ namespace Squidex.Infrastructure { try { - redisClient.Value.GetStatus(); + redisSubscriber = redis.GetSubscriber(); + + redis.GetStatus(); return TaskHelper.Done; } catch (Exception ex) { - throw new ConfigurationException($"Redis connection failed to connect to database {redisClient.Value.Configuration}", ex); + throw new ConfigurationException($"Redis connection failed to connect to database {redis.Configuration}", ex); } } @@ -66,7 +65,7 @@ namespace Squidex.Infrastructure { var typeName = typeof(T).FullName; - return (RedisSubscription)subscriptions.GetOrAdd(typeName, this, (k, c) => new RedisSubscription(c.redisSubscriber.Value, serializer, k, c.log)); + return (RedisSubscription)subscriptions.GetOrAdd(typeName, this, (k, c) => new RedisSubscription(c.redisSubscriber, serializer, k, c.log)); } } } diff --git a/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs b/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs index 954f26c4c..ddf8465e0 100644 --- a/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs +++ b/src/Squidex.Infrastructure/Assets/AssetAlreadyExistsException.cs @@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.Assets { Guard.NotNullOrEmpty(fileName, nameof(fileName)); - return $"An asset with name '{fileName}' already not exists."; + return $"An asset with name '{fileName}' already exists."; } } } diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs index 3bcf6ef5e..a61e24366 100644 --- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs @@ -7,7 +7,6 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure.Log; @@ -96,14 +95,14 @@ namespace Squidex.Infrastructure.Assets } } - public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) { - return UploadCoreAsync(GetFile(id, version, suffix), stream, ct); + return UploadCoreAsync(GetFile(id, version, suffix), stream, overwrite, ct); } public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default) { - return UploadCoreAsync(GetFile(fileName), stream, ct); + return UploadCoreAsync(GetFile(fileName), stream, false, ct); } public Task DeleteAsync(string id, long version, string suffix) @@ -123,11 +122,11 @@ namespace Squidex.Infrastructure.Assets return TaskHelper.Done; } - private static async Task UploadCoreAsync(FileInfo file, Stream stream, CancellationToken ct = default) + private static async Task UploadCoreAsync(FileInfo file, Stream stream, bool overwrite = false, CancellationToken ct = default) { try { - using (var fileStream = file.Open(FileMode.CreateNew, FileAccess.Write)) + using (var fileStream = file.Open(overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write)) { await stream.CopyToAsync(fileStream, BufferSize, ct); } @@ -159,7 +158,7 @@ namespace Squidex.Infrastructure.Assets private string GetPath(string id, long version, string suffix) { - return Path.Combine(directory.FullName, string.Join("_", new[] { id, version.ToString(), suffix }.Where(x => !string.IsNullOrWhiteSpace(x)))); + return Path.Combine(directory.FullName, StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix)); } } } diff --git a/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/src/Squidex.Infrastructure/Assets/IAssetStore.cs index b4170bfff..65d3c4f84 100644 --- a/src/Squidex.Infrastructure/Assets/IAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/IAssetStore.cs @@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.Assets Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default); - Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default); + Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default); Task DeleteAsync(string fileName); diff --git a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs new file mode 100644 index 000000000..bc3b8804e --- /dev/null +++ b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs @@ -0,0 +1,135 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class MemoryAssetStore : IAssetStore + { + private readonly ConcurrentDictionary streams = new ConcurrentDictionary(); + private readonly AsyncLock readerLock = new AsyncLock(); + private readonly AsyncLock writerLock = new AsyncLock(); + + public string GeneratePublicUrl(string id, long version, string suffix) + { + return null; + } + + public async Task CopyAsync(string sourceFileName, string id, long version, string suffix, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); + Guard.NotNullOrEmpty(id, nameof(id)); + + if (!streams.TryGetValue(sourceFileName, out var sourceStream)) + { + throw new AssetNotFoundException(sourceFileName); + } + + using (await readerLock.LockAsync()) + { + await UploadAsync(id, version, suffix, sourceStream, false, ct); + } + } + + public async Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + var fileName = GetFileName(id, version, suffix); + + if (!streams.TryGetValue(fileName, out var sourceStream)) + { + throw new AssetNotFoundException(fileName); + } + + using (await readerLock.LockAsync()) + { + try + { + await sourceStream.CopyToAsync(stream, 81920, ct); + } + finally + { + sourceStream.Position = 0; + } + } + } + + public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return UploadCoreAsync(GetFileName(id, version, suffix), stream, overwrite, ct); + } + + public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + return UploadCoreAsync(fileName, stream, false); + } + + private async Task UploadCoreAsync(string fileName, Stream stream, bool overwrite, CancellationToken ct = default) + { + var memoryStream = new MemoryStream(); + + async Task CopyAsync() + { + using (await writerLock.LockAsync()) + { + try + { + await stream.CopyToAsync(memoryStream, 81920, ct); + } + finally + { + memoryStream.Position = 0; + } + } + } + + if (overwrite) + { + await CopyAsync(); + + streams[fileName] = memoryStream; + } + else if (streams.TryAdd(fileName, memoryStream)) + { + await CopyAsync(); + } + else + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public Task DeleteAsync(string id, long version, string suffix) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return DeleteAsync(GetFileName(id, version, suffix)); + } + + public Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName, nameof(fileName)); + + streams.TryRemove(fileName, out _); + + return TaskHelper.Done; + } + + private string GetFileName(string id, long version, string suffix) + { + return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); + } + } +} diff --git a/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs b/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs new file mode 100644 index 000000000..b48eb7269 --- /dev/null +++ b/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class NoopAssetStore : IAssetStore + { + public string GeneratePublicUrl(string id, long version, string suffix) + { + return null; + } + + public Task CopyAsync(string sourceFileName, string id, long version, string suffix, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task UploadAsync(string id, long version, string suffix, Stream stream, bool overwrite = false, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task DeleteAsync(string fileName) + { + throw new NotSupportedException(); + } + + public Task DeleteAsync(string id, long version, string suffix) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Infrastructure/Lazier.cs b/src/Squidex.Infrastructure/AutoAssembyTypeProvider.cs similarity index 66% rename from src/Squidex.Infrastructure/Lazier.cs rename to src/Squidex.Infrastructure/AutoAssembyTypeProvider.cs index aae97240c..1cb4f5ee5 100644 --- a/src/Squidex.Infrastructure/Lazier.cs +++ b/src/Squidex.Infrastructure/AutoAssembyTypeProvider.cs @@ -5,16 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using Microsoft.Extensions.DependencyInjection; - namespace Squidex.Infrastructure { - public sealed class Lazier : Lazy where T : class + public sealed class AutoAssembyTypeProvider : ITypeProvider { - public Lazier(IServiceProvider provider) - : base(provider.GetRequiredService) + public void Map(TypeNameRegistry typeNameRegistry) { + typeNameRegistry.MapUnmapped(typeof(T).Assembly); } } } diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index 16da5a476..21d4a7436 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new() + public abstract class DomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new() { private readonly IStore store; private T snapshot = new T { Version = EtagVersion.Empty }; @@ -66,6 +66,9 @@ namespace Squidex.Infrastructure.Commands } } - protected abstract T OnEvent(Envelope @event); + protected T OnEvent(Envelope @event) + { + return Snapshot.Apply(@event); + } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs index 61501f7ac..5c3b0d717 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() + public abstract class DomainObjectGrainBase : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() { private readonly List> uncomittedEvents = new List>(); private readonly ISemanticLog log; diff --git a/src/Squidex.Infrastructure/Commands/IDomainState.cs b/src/Squidex.Infrastructure/Commands/IDomainState.cs index ee6891fe4..f20f14ce8 100644 --- a/src/Squidex.Infrastructure/Commands/IDomainState.cs +++ b/src/Squidex.Infrastructure/Commands/IDomainState.cs @@ -5,10 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure.EventSourcing; + namespace Squidex.Infrastructure.Commands { - public interface IDomainState + public interface IDomainState { long Version { get; set; } + + T Apply(Envelope @event); } } diff --git a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs index 10ab25586..8e820282f 100644 --- a/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new() + public abstract class LogSnapshotDomainObjectGrain : DomainObjectGrainBase where T : IDomainState, new() { private readonly IStore store; private readonly List snapshots = new List { new T { Version = EtagVersion.Empty } }; @@ -88,6 +88,9 @@ namespace Squidex.Infrastructure.Commands } } - protected abstract T OnEvent(Envelope @event); + protected T OnEvent(Envelope @event) + { + return Snapshot.Apply(@event); + } } } \ No newline at end of file diff --git a/src/Squidex/Config/Options.cs b/src/Squidex.Infrastructure/Configuration/Alternatives.cs similarity index 70% rename from src/Squidex/Config/Options.cs rename to src/Squidex.Infrastructure/Configuration/Alternatives.cs index 1962bba5e..77d70602f 100644 --- a/src/Squidex/Config/Options.cs +++ b/src/Squidex.Infrastructure/Configuration/Alternatives.cs @@ -1,18 +1,18 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; -namespace Squidex.Config +namespace Microsoft.Extensions.Configuration { - public sealed class Options : Dictionary + public sealed class Alternatives : Dictionary { - public Options() + public Alternatives() : base(StringComparer.OrdinalIgnoreCase) { } diff --git a/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs b/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs new file mode 100644 index 000000000..4c86b80a0 --- /dev/null +++ b/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Globalization; +using System.Linq; +using Squidex.Infrastructure; + +namespace Microsoft.Extensions.Configuration +{ + public static class ConfigurationExtensions + { + public static T GetOptionalValue(this IConfiguration config, string path, T defaultValue = default) + { + var value = config.GetValue(path, defaultValue); + + return value; + } + + public static int GetOptionalValue(this IConfiguration config, string path, int defaultValue) + { + var value = config.GetValue(path); + + if (string.IsNullOrWhiteSpace(value) || !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + result = defaultValue; + } + + return result; + } + + public static string GetRequiredValue(this IConfiguration config, string path) + { + var value = config.GetValue(path); + + if (string.IsNullOrWhiteSpace(value)) + { + var name = string.Join(" ", path.Split(':').Select(x => x.ToPascalCase())); + + throw new ConfigurationException($"Configure the {name} with '{path}'."); + } + + return value; + } + + public static string ConfigureByOption(this IConfiguration config, string path, Alternatives options) + { + var value = config.GetRequiredValue(path); + + if (options.TryGetValue(value, out var action)) + { + action(); + } + else if (options.TryGetValue("default", out action)) + { + action(); + } + else + { + throw new ConfigurationException($"Unsupported value '{value}' for '{path}', supported: {string.Join(" ", options.Keys)}."); + } + + return value; + } + } +} diff --git a/src/Squidex/Config/ServiceExtensions.cs b/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs similarity index 53% rename from src/Squidex/Config/ServiceExtensions.cs rename to src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs index 8d185bf60..9ee638ac0 100644 --- a/src/Squidex/Config/ServiceExtensions.cs +++ b/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs @@ -1,20 +1,18 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; -using System.Globalization; using System.Linq; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Squidex.Infrastructure; -namespace Squidex.Config +namespace Microsoft.Extensions.DependencyInjection { - public static class ServiceExtensions + public static class DependencyInjectionExtensions { public sealed class InterfaceRegistrator { @@ -30,6 +28,16 @@ namespace Squidex.Config return this; } + public InterfaceRegistrator AsOptional() + { + if (typeof(TInterface) != typeof(T)) + { + services.TryAddSingleton(typeof(TInterface), c => c.GetRequiredService()); + } + + return this; + } + public InterfaceRegistrator As() { if (typeof(TInterface) != typeof(T)) @@ -64,15 +72,6 @@ namespace Squidex.Config return new InterfaceRegistrator(services); } - public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services, T instance) where T : class - { - services.AddSingleton(typeof(T), instance); - - RegisterDefaults(services); - - return new InterfaceRegistrator(services); - } - public static InterfaceRegistrator AddSingletonAs(this IServiceCollection services) where T : class { services.AddSingleton(); @@ -84,60 +83,17 @@ namespace Squidex.Config private static void RegisterDefaults(IServiceCollection services) where T : class { - if (typeof(T).GetInterfaces().Contains(typeof(IInitializable))) - { - services.AddSingleton(typeof(IInitializable), c => c.GetRequiredService()); - } - } - - public static T GetOptionalValue(this IConfiguration config, string path, T defaultValue = default) - { - var value = config.GetValue(path, defaultValue); + var interfaces = typeof(T).GetInterfaces(); - return value; - } - - public static int GetOptionalValue(this IConfiguration config, string path, int defaultValue) - { - var value = config.GetValue(path); - var result = defaultValue; - - if (string.IsNullOrWhiteSpace(value) || !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) - { - result = defaultValue; - } - - return result; - } - - public static string GetRequiredValue(this IConfiguration config, string path) - { - var value = config.GetValue(path); - - if (string.IsNullOrWhiteSpace(value)) + if (interfaces.Contains(typeof(IInitializable))) { - var name = string.Join(' ', path.Split(':').Select(x => x.ToPascalCase())); - - throw new ConfigurationException($"Configure the {name} with '{path}'."); + services.AddSingleton(typeof(IInitializable), c => c.GetRequiredService()); } - return value; - } - - public static string ConfigureByOption(this IConfiguration config, string path, Options options) - { - var value = config.GetRequiredValue(path); - - if (options.TryGetValue(value, out var action)) + if (interfaces.Contains(typeof(IBackgroundProcess))) { - action(); + services.AddSingleton(typeof(IBackgroundProcess), c => c.GetRequiredService()); } - else - { - throw new ConfigurationException($"Unsupported value '{value}' for '{path}', supported: {string.Join(' ', options.Keys)}."); - } - - return value; } } } diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs index 5e0ed5067..6ee608632 100644 --- a/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs +++ b/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs @@ -28,8 +28,6 @@ namespace Squidex.Infrastructure.EventSourcing Task DeleteStreamAsync(string streamName); - Task DeleteManyAsync(string property, object value); - - IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null); + IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter = null, string position = null); } } diff --git a/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs b/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs new file mode 100644 index 000000000..b3bc063af --- /dev/null +++ b/src/Squidex.Infrastructure/EventSourcing/StreamFilter.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.EventSourcing +{ + public static class StreamFilter + { + public static bool IsAll(string filter) + { + return string.IsNullOrWhiteSpace(filter) + || string.Equals(filter, ".*", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter, "(.*)", StringComparison.OrdinalIgnoreCase) + || string.Equals(filter, "(.*?)", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Squidex.Infrastructure/IBackgroundProcess.cs b/src/Squidex.Infrastructure/IBackgroundProcess.cs new file mode 100644 index 000000000..38003910c --- /dev/null +++ b/src/Squidex.Infrastructure/IBackgroundProcess.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure +{ + public interface IBackgroundProcess + { + Task StartAsync(CancellationToken ct); + } +} diff --git a/src/Squidex.Infrastructure/ITypeProvider.cs b/src/Squidex.Infrastructure/ITypeProvider.cs new file mode 100644 index 000000000..63ee58238 --- /dev/null +++ b/src/Squidex.Infrastructure/ITypeProvider.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure +{ + public interface ITypeProvider + { + void Map(TypeNameRegistry typeNameRegistry); + } +} diff --git a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs b/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs index 9763aabf8..9d13c43b8 100644 --- a/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs +++ b/src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs @@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.Log private readonly string applicationSessionId; public ApplicationInfoLogAppender(Type type, Guid applicationSession) - : this(type?.GetTypeInfo().Assembly, applicationSession) + : this(type?.Assembly, applicationSession) { } diff --git a/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs b/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs index 7c232bdf6..1b58a9d02 100644 --- a/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs +++ b/src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs @@ -126,8 +126,16 @@ namespace Squidex.Infrastructure.Log return writer.WriteObject(nameof(exception), exception, (ctx, w) => { w.WriteProperty("type", ctx.GetType().FullName); - w.WriteProperty("message", ctx.Message); - w.WriteProperty("stackTrace", ctx.StackTrace); + + if (ctx.Message != null) + { + w.WriteProperty("message", ctx.Message); + } + + if (ctx.StackTrace != null) + { + w.WriteProperty("stackTrace", ctx.StackTrace); + } }); } diff --git a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs b/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs similarity index 79% rename from src/Squidex.Infrastructure/Orleans/Bootstrap.cs rename to src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs index 5a3bb7a19..ae47858a5 100644 --- a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs +++ b/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs @@ -12,19 +12,19 @@ using Orleans.Runtime; namespace Squidex.Infrastructure.Orleans { - public sealed class Bootstrap : IStartupTask where T : IBackgroundGrain + public sealed class GrainBootstrap : IBackgroundProcess where T : IBackgroundGrain { private const int NumTries = 10; private readonly IGrainFactory grainFactory; - public Bootstrap(IGrainFactory grainFactory) + public GrainBootstrap(IGrainFactory grainFactory) { Guard.NotNull(grainFactory, nameof(grainFactory)); this.grainFactory = grainFactory; } - public async Task Execute(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken ct = default) { for (var i = 1; i <= NumTries; i++) { @@ -45,5 +45,10 @@ namespace Squidex.Infrastructure.Orleans } } } + + public override string ToString() + { + return typeof(T).ToString(); + } } } diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs b/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs index 0a816c381..a179c78d8 100644 --- a/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs +++ b/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs @@ -16,7 +16,7 @@ namespace Squidex.Infrastructure.Orleans { public Guid Key { get; private set; } - public sealed override Task OnActivateAsync() + public override Task OnActivateAsync() { return ActivateAsync(this.GetPrimaryKey()); } diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfString.cs b/src/Squidex.Infrastructure/Orleans/GrainOfString.cs index 041f67df5..13d737ae0 100644 --- a/src/Squidex.Infrastructure/Orleans/GrainOfString.cs +++ b/src/Squidex.Infrastructure/Orleans/GrainOfString.cs @@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.Orleans { public string Key { get; private set; } - public sealed override Task OnActivateAsync() + public override Task OnActivateAsync() { return ActivateAsync(this.GetPrimaryKeyString()); } diff --git a/src/Squidex.Infrastructure/Plugins/IPlugin.cs b/src/Squidex.Infrastructure/Plugins/IPlugin.cs new file mode 100644 index 000000000..9d5a2618e --- /dev/null +++ b/src/Squidex.Infrastructure/Plugins/IPlugin.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Infrastructure.Plugins +{ + public interface IPlugin + { + void ConfigureServices(IServiceCollection services, IConfiguration config); + } +} diff --git a/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs b/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs new file mode 100644 index 000000000..f93e3eadb --- /dev/null +++ b/src/Squidex.Infrastructure/Plugins/IWebPlugin.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; + +namespace Squidex.Infrastructure.Plugins +{ + public interface IWebPlugin : IPlugin + { + void Configure(IApplicationBuilder app); + } +} diff --git a/src/Squidex.Infrastructure/Plugins/PluginManager.cs b/src/Squidex.Infrastructure/Plugins/PluginManager.cs new file mode 100644 index 000000000..34c809c69 --- /dev/null +++ b/src/Squidex.Infrastructure/Plugins/PluginManager.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Plugins +{ + public sealed class PluginManager + { + private readonly HashSet loadedPlugins = new HashSet(); + private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>(); + + public IReadOnlyCollection Plugins + { + get { return loadedPlugins; } + } + + public void Add(string name, Assembly assembly) + { + Guard.NotNull(assembly, nameof(assembly)); + + var pluginTypes = + assembly.GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t)) + .Where(t => !t.IsAbstract); + + foreach (var pluginType in pluginTypes) + { + try + { + var plugin = (IPlugin)Activator.CreateInstance(pluginType); + + loadedPlugins.Add(plugin); + } + catch (Exception ex) + { + LogException(name, "Instantiating", ex); + } + } + } + + public void LogException(string plugin, string action, Exception exception) + { + Guard.NotNull(plugin, nameof(plugin)); + Guard.NotNull(action, nameof(action)); + Guard.NotNull(exception, nameof(exception)); + + exceptions.Add((plugin, action, exception)); + } + + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + Guard.NotNull(services, nameof(services)); + Guard.NotNull(config, nameof(config)); + + foreach (var plugin in loadedPlugins) + { + plugin.ConfigureServices(services, config); + } + } + + public void Configure(IApplicationBuilder app) + { + Guard.NotNull(app, nameof(app)); + + foreach (var plugin in loadedPlugins.OfType()) + { + plugin.Configure(app); + } + } + + public void Log(ISemanticLog log) + { + Guard.NotNull(log, nameof(log)); + + if (loadedPlugins.Count > 0 || exceptions.Count > 0) + { + var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed"; + + log.LogInformation(w => w + .WriteProperty("action", "pluginsLoaded") + .WriteProperty("status", status) + .WriteArray("errors", e => + { + foreach (var error in exceptions) + { + e.WriteObject(x => x + .WriteProperty("plugin", error.Plugin) + .WriteProperty("action", error.Action) + .WriteException(error.Exception)); + } + }) + .WriteArray("plugins", a => + { + foreach (var plugin in loadedPlugins) + { + a.WriteValue(plugin.GetType().ToString()); + } + })); + } + } + } +} diff --git a/src/Squidex.Infrastructure/Plugins/PluginOptions.cs b/src/Squidex.Infrastructure/Plugins/PluginOptions.cs new file mode 100644 index 000000000..561e0845a --- /dev/null +++ b/src/Squidex.Infrastructure/Plugins/PluginOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Plugins +{ + public sealed class PluginOptions + { + public string[] Plugins { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs b/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs index 474f55355..55354eb13 100644 --- a/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs +++ b/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs @@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.Reflection BindingFlags.Public | BindingFlags.Instance; - if (!type.GetTypeInfo().IsInterface) + if (!type.IsInterface) { return type.GetProperties(bindingFlags); } diff --git a/src/Squidex.Infrastructure/Singletons.cs b/src/Squidex.Infrastructure/Singletons.cs index 494157155..e0eb715ab 100644 --- a/src/Squidex.Infrastructure/Singletons.cs +++ b/src/Squidex.Infrastructure/Singletons.cs @@ -18,10 +18,5 @@ namespace Squidex.Infrastructure { return Instances.GetOrAdd(key, factory); } - - public static Lazy GetOrAddLazy(string key, Func factory) - { - return new Lazy(() => Instances.GetOrAdd(key, factory)); - } } } diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index ea5c0a8b0..2ac2b92d0 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -8,16 +8,17 @@ True + - + all runtime; build; native; contentfiles; analyzers - - + + @@ -26,7 +27,7 @@ - + diff --git a/src/Squidex.Infrastructure/SquidexInfrastructure.cs b/src/Squidex.Infrastructure/SquidexInfrastructure.cs index 8dac58f91..a76b688dc 100644 --- a/src/Squidex.Infrastructure/SquidexInfrastructure.cs +++ b/src/Squidex.Infrastructure/SquidexInfrastructure.cs @@ -7,9 +7,11 @@ using System.Reflection; +#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. + namespace Squidex.Infrastructure { - public static class SquidexInfrastructure + public sealed class SquidexInfrastructure { public static readonly Assembly Assembly = typeof(SquidexInfrastructure).Assembly; } diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/src/Squidex.Infrastructure/States/ISnapshotStore.cs index 38646e64f..68243db74 100644 --- a/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading; using System.Threading.Tasks; namespace Squidex.Infrastructure.States @@ -20,6 +21,6 @@ namespace Squidex.Infrastructure.States Task RemoveAsync(TKey key); - Task ReadAllAsync(Func callback); + Task ReadAllAsync(Func callback, CancellationToken ct = default); } } diff --git a/src/Squidex.Infrastructure/StringExtensions.cs b/src/Squidex.Infrastructure/StringExtensions.cs index a1c3b100d..eb0c309bd 100644 --- a/src/Squidex.Infrastructure/StringExtensions.cs +++ b/src/Squidex.Infrastructure/StringExtensions.cs @@ -16,9 +16,11 @@ namespace Squidex.Infrastructure public static class StringExtensions { private const char NullChar = (char)0; + private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled); private static readonly Regex EmailRegex = new Regex("^[a-zA-Z0-9.!#$%&’*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$", RegexOptions.Compiled); private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); + private static readonly Dictionary LowerCaseDiacritics; private static readonly Dictionary Diacritics = new Dictionary { @@ -555,6 +557,8 @@ namespace Squidex.Infrastructure public static string BuildFullUrl(this string baseUrl, string path, bool trailingSlash = false) { + Guard.NotNull(path, nameof(path)); + var url = $"{baseUrl.TrimEnd('/')}/{path.Trim('/')}"; if (trailingSlash && @@ -567,5 +571,34 @@ namespace Squidex.Infrastructure return url; } + + public static string JoinNonEmpty(string separator, params string[] parts) + { + Guard.NotNull(separator, nameof(separator)); + + if (parts == null || parts.Length == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(); + + for (var i = 0; i < parts.Length; i++) + { + var part = parts[i]; + + if (!string.IsNullOrWhiteSpace(part)) + { + sb.Append(part); + + if (i < parts.Length - 1) + { + sb.Append(separator); + } + } + } + + return sb.ToString(); + } } } diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs index 877e76d7a..5409faca1 100644 --- a/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs +++ b/src/Squidex.Infrastructure/Translations/DeepLTranslator.cs @@ -10,6 +10,7 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Squidex.Infrastructure.Json; namespace Squidex.Infrastructure.Translations @@ -18,7 +19,7 @@ namespace Squidex.Infrastructure.Translations { private const string Url = "https://api.deepl.com/v2/translate"; private readonly HttpClient httpClient = new HttpClient(); - private readonly string authKey; + private readonly DeepLTranslatorOptions deepLOptions; private readonly IJsonSerializer jsonSerializer; private sealed class Response @@ -31,12 +32,12 @@ namespace Squidex.Infrastructure.Translations public string Text { get; set; } } - public DeepLTranslator(string authKey, IJsonSerializer jsonSerializer) + public DeepLTranslator(IOptions deepLOptions, IJsonSerializer jsonSerializer) { - Guard.NotNull(authKey, nameof(authKey)); + Guard.NotNull(deepLOptions, nameof(deepLOptions)); Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - this.authKey = authKey; + this.deepLOptions = deepLOptions.Value; this.jsonSerializer = jsonSerializer; } @@ -48,9 +49,14 @@ namespace Squidex.Infrastructure.Translations return new Translation(TranslationResult.NotTranslated, sourceText); } + if (string.IsNullOrWhiteSpace(deepLOptions.AuthKey)) + { + return new Translation(TranslationResult.NotImplemented); + } + var parameters = new Dictionary { - ["auth_key"] = authKey, + ["auth_key"] = deepLOptions.AuthKey, ["text"] = sourceText, ["target_lang"] = GetLanguageCode(targetLanguage) }; @@ -81,7 +87,7 @@ namespace Squidex.Infrastructure.Translations return new Translation(TranslationResult.Failed, resultText: responseString); } - private string GetLanguageCode(Language language) + private static string GetLanguageCode(Language language) { return language.Iso2Code.Substring(0, 2).ToUpperInvariant(); } diff --git a/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs b/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs new file mode 100644 index 000000000..d7124e343 --- /dev/null +++ b/src/Squidex.Infrastructure/Translations/DeepLTranslatorOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Translations +{ + public sealed class DeepLTranslatorOptions + { + public string AuthKey { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/TypeNameRegistry.cs b/src/Squidex.Infrastructure/TypeNameRegistry.cs index 233f5ee28..5df5f9f00 100644 --- a/src/Squidex.Infrastructure/TypeNameRegistry.cs +++ b/src/Squidex.Infrastructure/TypeNameRegistry.cs @@ -16,6 +16,17 @@ namespace Squidex.Infrastructure private readonly Dictionary namesByType = new Dictionary(); private readonly Dictionary typesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); + public TypeNameRegistry(IEnumerable providers = null) + { + if (providers != null) + { + foreach (var provider in providers) + { + Map(provider); + } + } + } + public TypeNameRegistry MapObsolete(Type type, string name) { Guard.NotNull(type, nameof(type)); @@ -36,11 +47,20 @@ namespace Squidex.Infrastructure return this; } + public TypeNameRegistry Map(ITypeProvider provider) + { + Guard.NotNull(provider, nameof(provider)); + + provider.Map(this); + + return this; + } + public TypeNameRegistry Map(Type type) { Guard.NotNull(type, nameof(type)); - var typeNameAttribute = type.GetTypeInfo().GetCustomAttribute(); + var typeNameAttribute = type.GetCustomAttribute(); if (!string.IsNullOrWhiteSpace(typeNameAttribute?.TypeName)) { diff --git a/src/Squidex.Web/ApiController.cs b/src/Squidex.Web/ApiController.cs new file mode 100644 index 000000000..5eb10a7bf --- /dev/null +++ b/src/Squidex.Web/ApiController.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web +{ + [Area("Api")] + [ApiController] + [ApiExceptionFilter] + [ApiModelValidation(false)] + public abstract class ApiController : Controller + { + protected ICommandBus CommandBus { get; } + + protected IAppEntity App + { + get + { + var appFeature = HttpContext.Features.Get(); + + if (appFeature == null) + { + throw new InvalidOperationException("Not in a app context."); + } + + return appFeature.App; + } + } + + protected Guid AppId + { + get { return App.Id; } + } + + protected ApiController(ICommandBus commandBus) + { + Guard.NotNull(commandBus, nameof(commandBus)); + + CommandBus = commandBus; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.HttpContext.Request.PathBase.StartsWithSegments("/api")) + { + context.Result = new RedirectResult("/"); + } + } + } +} diff --git a/src/Squidex/Pipeline/ApiCostsAttribute.cs b/src/Squidex.Web/ApiCostsAttribute.cs similarity index 93% rename from src/Squidex/Pipeline/ApiCostsAttribute.cs rename to src/Squidex.Web/ApiCostsAttribute.cs index 07cc48f18..0b07afc23 100644 --- a/src/Squidex/Pipeline/ApiCostsAttribute.cs +++ b/src/Squidex.Web/ApiCostsAttribute.cs @@ -7,8 +7,9 @@ using System; using Microsoft.AspNetCore.Mvc; +using Squidex.Web.Pipeline; -namespace Squidex.Pipeline +namespace Squidex.Web { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public sealed class ApiCostsAttribute : ServiceFilterAttribute, IApiCostsFeature diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex.Web/ApiExceptionFilterAttribute.cs similarity index 86% rename from src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs rename to src/Squidex.Web/ApiExceptionFilterAttribute.cs index a681860ad..3e195c0be 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex.Web/ApiExceptionFilterAttribute.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Infrastructure; -namespace Squidex.Pipeline +namespace Squidex.Web { public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter { @@ -60,7 +61,7 @@ namespace Squidex.Pipeline private static IActionResult OnValidationException(ValidationException ex) { - return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ex.Errors?.ToArray(e => e.Message) }); + return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ToDetails(ex) }); } private static IActionResult ErrorResult(int statusCode, ErrorDto error) @@ -89,5 +90,20 @@ namespace Squidex.Pipeline context.Result = result; } } + + private static string[] ToDetails(ValidationException ex) + { + return ex.Errors?.ToArray(e => + { + if (e.PropertyNames?.Any() == true) + { + return $"{string.Join(", ", e.PropertyNames)}: {e.Message}"; + } + else + { + return e.Message; + } + }); + } } } diff --git a/src/Squidex/Pipeline/ApiModelValidationAttribute.cs b/src/Squidex.Web/ApiModelValidationAttribute.cs similarity index 98% rename from src/Squidex/Pipeline/ApiModelValidationAttribute.cs rename to src/Squidex.Web/ApiModelValidationAttribute.cs index 41bb86156..df1a5a994 100644 --- a/src/Squidex/Pipeline/ApiModelValidationAttribute.cs +++ b/src/Squidex.Web/ApiModelValidationAttribute.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Newtonsoft.Json; using Squidex.Infrastructure; -namespace Squidex.Pipeline +namespace Squidex.Web { public sealed class ApiModelValidationAttribute : ActionFilterAttribute { diff --git a/src/Squidex/Pipeline/ApiPermissionAttribute.cs b/src/Squidex.Web/ApiPermissionAttribute.cs similarity index 88% rename from src/Squidex/Pipeline/ApiPermissionAttribute.cs rename to src/Squidex.Web/ApiPermissionAttribute.cs index 48cd75e62..d515c76ce 100644 --- a/src/Squidex/Pipeline/ApiPermissionAttribute.cs +++ b/src/Squidex.Web/ApiPermissionAttribute.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using IdentityServer4.AccessTokenValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -15,7 +14,7 @@ using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Tasks; using Squidex.Shared.Identity; -namespace Squidex.Pipeline +namespace Squidex.Web { public sealed class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter { @@ -28,7 +27,7 @@ namespace Squidex.Pipeline public ApiPermissionAttribute(params string[] ids) { - AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme; + AuthenticationSchemes = "Bearer"; permissionIds = ids; } @@ -50,7 +49,11 @@ namespace Squidex.Pipeline id = id.Replace($"{{{routeParam.Key}}}", routeParam.Value?.ToString()); } - hasPermission |= set.Allows(new Permission(id)); + if (set.Allows(new Permission(id))) + { + hasPermission = true; + break; + } } if (!hasPermission) diff --git a/src/Squidex/Pipeline/AssetRequestSizeLimitAttribute.cs b/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs similarity index 98% rename from src/Squidex/Pipeline/AssetRequestSizeLimitAttribute.cs rename to src/Squidex.Web/AssetRequestSizeLimitAttribute.cs index 17236e81a..2cc5479c4 100644 --- a/src/Squidex/Pipeline/AssetRequestSizeLimitAttribute.cs +++ b/src/Squidex.Web/AssetRequestSizeLimitAttribute.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Assets; -namespace Squidex.Pipeline +namespace Squidex.Web { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public sealed class AssetRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter diff --git a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs similarity index 97% rename from src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs rename to src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs index ef14a8bb0..0ebb669f3 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs +++ b/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs @@ -13,7 +13,7 @@ using Microsoft.Net.Http.Headers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Pipeline.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares { public class ETagCommandMiddleware : ICommandMiddleware { diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs similarity index 97% rename from src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs rename to src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs index 4baf4b814..0bff3a6c5 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs +++ b/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; -namespace Squidex.Pipeline.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares { public class EnrichWithActorCommandMiddleware : ICommandMiddleware { diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs similarity index 97% rename from src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs rename to src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs index f41214737..06e050784 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs +++ b/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Pipeline.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares { public sealed class EnrichWithAppIdCommandMiddleware : ICommandMiddleware { diff --git a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs similarity index 98% rename from src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs rename to src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs index f2d5ae42b..672a16b74 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ b/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -14,7 +14,7 @@ using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -namespace Squidex.Pipeline.CommandMiddlewares +namespace Squidex.Web.CommandMiddlewares { public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware { diff --git a/src/Squidex/Config/Constants.cs b/src/Squidex.Web/Constants.cs similarity index 94% rename from src/Squidex/Config/Constants.cs rename to src/Squidex.Web/Constants.cs index ee9aa76e8..770c8a435 100644 --- a/src/Squidex/Config/Constants.cs +++ b/src/Squidex.Web/Constants.cs @@ -5,9 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using IdentityServer4.Models; +using Squidex.Infrastructure; -namespace Squidex.Config +namespace Squidex.Web { public static class Constants { @@ -35,7 +35,7 @@ namespace Squidex.Config public static readonly string InternalClientId = "squidex-internal"; - public static readonly string InternalClientSecret = "squidex-internal".Sha256(); + public static readonly string InternalClientSecret = "squidex-internal".Sha256Base64(); public static readonly string IdentityServerPrefix = "/identity-server"; } diff --git a/src/Squidex/Pipeline/ETagExtensions.cs b/src/Squidex.Web/ETagExtensions.cs similarity index 98% rename from src/Squidex/Pipeline/ETagExtensions.cs rename to src/Squidex.Web/ETagExtensions.cs index d9f1d726c..5ee961a9d 100644 --- a/src/Squidex/Pipeline/ETagExtensions.cs +++ b/src/Squidex.Web/ETagExtensions.cs @@ -11,7 +11,7 @@ using System.Text; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; -namespace Squidex.Pipeline +namespace Squidex.Web { public static class ETagExtensions { diff --git a/src/Squidex/Pipeline/ETagFilter.cs b/src/Squidex.Web/ETagFilter.cs similarity index 98% rename from src/Squidex/Pipeline/ETagFilter.cs rename to src/Squidex.Web/ETagFilter.cs index d14561734..b76772ad3 100644 --- a/src/Squidex/Pipeline/ETagFilter.cs +++ b/src/Squidex.Web/ETagFilter.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; -namespace Squidex.Pipeline +namespace Squidex.Web { public sealed class ETagFilter : IAsyncActionFilter { diff --git a/src/Squidex/Pipeline/ETagOptions.cs b/src/Squidex.Web/ETagOptions.cs similarity index 94% rename from src/Squidex/Pipeline/ETagOptions.cs rename to src/Squidex.Web/ETagOptions.cs index c54288e77..8e832dbca 100644 --- a/src/Squidex/Pipeline/ETagOptions.cs +++ b/src/Squidex.Web/ETagOptions.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Pipeline +namespace Squidex.Web { public sealed class ETagOptions { diff --git a/src/Squidex/Pipeline/EntityCreatedDto.cs b/src/Squidex.Web/EntityCreatedDto.cs similarity index 80% rename from src/Squidex/Pipeline/EntityCreatedDto.cs rename to src/Squidex.Web/EntityCreatedDto.cs index 9b768e652..95738823d 100644 --- a/src/Squidex/Pipeline/EntityCreatedDto.cs +++ b/src/Squidex.Web/EntityCreatedDto.cs @@ -8,19 +8,15 @@ using System.ComponentModel.DataAnnotations; using Squidex.Infrastructure.Commands; -namespace Squidex.Pipeline +namespace Squidex.Web { public sealed class EntityCreatedDto { - /// - /// Id of the created entity. - /// [Required] + [Display(Description = "Id of the created entity.")] public string Id { get; set; } - - /// - /// The new version of the entity. - /// + + [Display(Description = "The new version of the entity.")] public long Version { get; set; } public static EntityCreatedDto FromResult(EntityCreatedResult result) diff --git a/src/Squidex/Pipeline/ErrorDto.cs b/src/Squidex.Web/ErrorDto.cs similarity index 69% rename from src/Squidex/Pipeline/ErrorDto.cs rename to src/Squidex.Web/ErrorDto.cs index 535a9e602..2d3e8f6be 100644 --- a/src/Squidex/Pipeline/ErrorDto.cs +++ b/src/Squidex.Web/ErrorDto.cs @@ -7,24 +7,18 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Pipeline +namespace Squidex.Web { public sealed class ErrorDto { - /// - /// Error message. - /// [Required] + [Display(Description = "Error message.")] public string Message { get; set; } - - /// - /// Detailed error messages. - /// + + [Display(Description = "Detailed error messages.")] public string[] Details { get; set; } - - /// - /// Status code of the http response. - /// + + [Display(Description = "Status code of the http response.")] public int? StatusCode { get; set; } = 400; } } diff --git a/src/Squidex/Pipeline/Extensions.cs b/src/Squidex.Web/Extensions.cs similarity index 96% rename from src/Squidex/Pipeline/Extensions.cs rename to src/Squidex.Web/Extensions.cs index 98f693727..b7f7594bf 100644 --- a/src/Squidex/Pipeline/Extensions.cs +++ b/src/Squidex.Web/Extensions.cs @@ -6,10 +6,9 @@ // ========================================================================== using System.Security.Claims; -using Squidex.Config; using Squidex.Infrastructure.Security; -namespace Squidex.Pipeline +namespace Squidex.Web { public static class Extensions { diff --git a/src/Squidex/Pipeline/FileCallbackResult.cs b/src/Squidex.Web/FileCallbackResult.cs similarity index 93% rename from src/Squidex/Pipeline/FileCallbackResult.cs rename to src/Squidex.Web/FileCallbackResult.cs index 5cc764a99..5ca752eea 100644 --- a/src/Squidex/Pipeline/FileCallbackResult.cs +++ b/src/Squidex.Web/FileCallbackResult.cs @@ -10,8 +10,9 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; +using Squidex.Web.Pipeline; -namespace Squidex.Pipeline +namespace Squidex.Web { public sealed class FileCallbackResult : FileResult { diff --git a/src/Squidex/Pipeline/IApiCostsFeature.cs b/src/Squidex.Web/IApiCostsFeature.cs similarity index 94% rename from src/Squidex/Pipeline/IApiCostsFeature.cs rename to src/Squidex.Web/IApiCostsFeature.cs index f4cfa1b97..9f1e380f5 100644 --- a/src/Squidex/Pipeline/IApiCostsFeature.cs +++ b/src/Squidex.Web/IApiCostsFeature.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Pipeline +namespace Squidex.Web { public interface IApiCostsFeature { diff --git a/src/Squidex/Pipeline/IAppFeature.cs b/src/Squidex.Web/IAppFeature.cs similarity index 94% rename from src/Squidex/Pipeline/IAppFeature.cs rename to src/Squidex.Web/IAppFeature.cs index d8a23b925..a798da598 100644 --- a/src/Squidex/Pipeline/IAppFeature.cs +++ b/src/Squidex.Web/IAppFeature.cs @@ -7,7 +7,7 @@ using Squidex.Domain.Apps.Entities.Apps; -namespace Squidex.Pipeline +namespace Squidex.Web { public interface IAppFeature { diff --git a/src/Squidex/Pipeline/IGenerateEtag.cs b/src/Squidex.Web/IGenerateEtag.cs similarity index 94% rename from src/Squidex/Pipeline/IGenerateEtag.cs rename to src/Squidex.Web/IGenerateEtag.cs index e1f0c8789..6986f1acc 100644 --- a/src/Squidex/Pipeline/IGenerateEtag.cs +++ b/src/Squidex.Web/IGenerateEtag.cs @@ -7,7 +7,7 @@ using System; -namespace Squidex.Pipeline +namespace Squidex.Web { public interface IGenerateETag { diff --git a/src/Squidex/Areas/Api/Controllers/MyJsonInheritanceConverter.cs b/src/Squidex.Web/MyJsonInheritanceConverter.cs similarity index 98% rename from src/Squidex/Areas/Api/Controllers/MyJsonInheritanceConverter.cs rename to src/Squidex.Web/MyJsonInheritanceConverter.cs index ed16e20b3..ff3a1854e 100644 --- a/src/Squidex/Areas/Api/Controllers/MyJsonInheritanceConverter.cs +++ b/src/Squidex.Web/MyJsonInheritanceConverter.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure; #pragma warning disable RECS0108 // Warns about static fields in generic types -namespace Squidex.Areas.Api.Controllers +namespace Squidex.Web { public class MyJsonInheritanceConverter : JsonInheritanceConverter { diff --git a/src/Squidex/Pipeline/ActionContextLogAppender.cs b/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs similarity index 93% rename from src/Squidex/Pipeline/ActionContextLogAppender.cs rename to src/Squidex.Web/Pipeline/ActionContextLogAppender.cs index 97c6060b3..8db6973d1 100644 --- a/src/Squidex/Pipeline/ActionContextLogAppender.cs +++ b/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; using Squidex.Infrastructure.Log; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class ActionContextLogAppender : ILogAppender { @@ -57,9 +57,9 @@ namespace Squidex.Pipeline { w.WriteObject("routeValues", actionContext.ActionDescriptor.RouteValues, (routeValues, r) => { - foreach (var (key, val) in routeValues) + foreach (var kvp in routeValues) { - r.WriteProperty(key, val); + r.WriteProperty(kvp.Key, kvp.Value); } }); } diff --git a/src/Squidex/Pipeline/ApiCostsFilter.cs b/src/Squidex.Web/Pipeline/ApiCostsFilter.cs similarity index 98% rename from src/Squidex/Pipeline/ApiCostsFilter.cs rename to src/Squidex.Web/Pipeline/ApiCostsFilter.cs index 2dc93f326..0859a26c3 100644 --- a/src/Squidex/Pipeline/ApiCostsFilter.cs +++ b/src/Squidex.Web/Pipeline/ApiCostsFilter.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.UsageTracking; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class ApiCostsFilter : IAsyncActionFilter, IFilterContainer { diff --git a/src/Squidex/Pipeline/ApiPermissionUnifier.cs b/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs similarity index 90% rename from src/Squidex/Pipeline/ApiPermissionUnifier.cs rename to src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs index 98be130af..672022d73 100644 --- a/src/Squidex/Pipeline/ApiPermissionUnifier.cs +++ b/src/Squidex.Web/Pipeline/ApiPermissionUnifier.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -12,7 +13,7 @@ using Microsoft.AspNetCore.Authentication; using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class ApiPermissionUnifier : IClaimsTransformation { @@ -22,7 +23,7 @@ namespace Squidex.Pipeline { var identity = principal.Identities.First(); - if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, AdministratorRole)) + if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, AdministratorRole, StringComparison.OrdinalIgnoreCase)) { identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)); } diff --git a/src/Squidex/Pipeline/AppResolver.cs b/src/Squidex.Web/Pipeline/AppResolver.cs similarity index 90% rename from src/Squidex/Pipeline/AppResolver.cs rename to src/Squidex.Web/Pipeline/AppResolver.cs index 705bfae79..cc0ae853b 100644 --- a/src/Squidex/Pipeline/AppResolver.cs +++ b/src/Squidex.Web/Pipeline/AppResolver.cs @@ -17,7 +17,7 @@ using Squidex.Infrastructure.Security; using Squidex.Shared; using Squidex.Shared.Identity; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class AppResolver : IAsyncActionFilter { @@ -61,17 +61,6 @@ namespace Squidex.Pipeline (role, permissions) = FindByOpenIdClient(app, user); } - if (permissions == null || permissions.Count == 0) - { - var set = user.Permissions(); - - if (!set.Includes(Permissions.ForApp(Permissions.App, appName)) && !AllowAnonymous(context)) - { - context.Result = new NotFoundResult(); - return; - } - } - if (permissions != null) { var identity = user.Identities.First(); @@ -84,6 +73,14 @@ namespace Squidex.Pipeline } } + var set = user.Permissions(); + + if (!set.Includes(Permissions.ForApp(Permissions.App, appName))&& !AllowAnonymous(context)) + { + context.Result = new NotFoundResult(); + return; + } + context.HttpContext.Features.Set(new AppFeature(app)); } diff --git a/src/Squidex/Pipeline/CleanupHostMiddleware.cs b/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs similarity index 97% rename from src/Squidex/Pipeline/CleanupHostMiddleware.cs rename to src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs index 504a5a455..82c98ad21 100644 --- a/src/Squidex/Pipeline/CleanupHostMiddleware.cs +++ b/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public class CleanupHostMiddleware { diff --git a/src/Squidex/Pipeline/EnforceHttpsMiddleware.cs b/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs similarity index 88% rename from src/Squidex/Pipeline/EnforceHttpsMiddleware.cs rename to src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs index 4f4c6158e..499b683f2 100644 --- a/src/Squidex/Pipeline/EnforceHttpsMiddleware.cs +++ b/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs @@ -9,15 +9,14 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -using Squidex.Config; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class EnforceHttpsMiddleware : IMiddleware { - private readonly IOptions urls; + private readonly IOptions urls; - public EnforceHttpsMiddleware(IOptions urls) + public EnforceHttpsMiddleware(IOptions urls) { this.urls = urls; } diff --git a/src/Squidex/Pipeline/FileCallbackResultExecutor.cs b/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs similarity index 97% rename from src/Squidex/Pipeline/FileCallbackResultExecutor.cs rename to src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs index 5033af37f..fc8b011d7 100644 --- a/src/Squidex/Pipeline/FileCallbackResultExecutor.cs +++ b/src/Squidex.Web/Pipeline/FileCallbackResultExecutor.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Logging; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class FileCallbackResultExecutor : FileResultExecutorBase { diff --git a/src/Squidex/Pipeline/LocalCacheMiddleware.cs b/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs similarity index 97% rename from src/Squidex/Pipeline/LocalCacheMiddleware.cs rename to src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs index 064e6a520..b9f050191 100644 --- a/src/Squidex/Pipeline/LocalCacheMiddleware.cs +++ b/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class LocalCacheMiddleware : IMiddleware { diff --git a/src/Squidex/Pipeline/MeasureResultFilter.cs b/src/Squidex.Web/Pipeline/MeasureResultFilter.cs similarity index 97% rename from src/Squidex/Pipeline/MeasureResultFilter.cs rename to src/Squidex.Web/Pipeline/MeasureResultFilter.cs index 2d22afd65..de2220f32 100644 --- a/src/Squidex/Pipeline/MeasureResultFilter.cs +++ b/src/Squidex.Web/Pipeline/MeasureResultFilter.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Infrastructure.Log; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class MeasureResultFilter : IAsyncResultFilter, IAsyncActionFilter { diff --git a/src/Squidex/Pipeline/RequestLogPerformanceMiddleware.cs b/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs similarity index 98% rename from src/Squidex/Pipeline/RequestLogPerformanceMiddleware.cs rename to src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs index 87915019a..c2e3c7006 100644 --- a/src/Squidex/Pipeline/RequestLogPerformanceMiddleware.cs +++ b/src/Squidex.Web/Pipeline/RequestLogPerformanceMiddleware.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Security; -namespace Squidex.Pipeline +namespace Squidex.Web.Pipeline { public sealed class RequestLogPerformanceMiddleware : IMiddleware { diff --git a/src/Squidex/Pipeline/UrlGenerator.cs b/src/Squidex.Web/Services/UrlGenerator.cs similarity index 92% rename from src/Squidex/Pipeline/UrlGenerator.cs rename to src/Squidex.Web/Services/UrlGenerator.cs index d2c1266e7..ef2bab32d 100644 --- a/src/Squidex/Pipeline/UrlGenerator.cs +++ b/src/Squidex.Web/Services/UrlGenerator.cs @@ -7,7 +7,6 @@ using System; using Microsoft.Extensions.Options; -using Squidex.Config; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Entities.Apps; @@ -18,16 +17,16 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; -namespace Squidex.Pipeline +namespace Squidex.Web.Services { public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator { private readonly IAssetStore assetStore; - private readonly MyUrlsOptions urlsOptions; + private readonly UrlsOptions urlsOptions; public bool CanGenerateAssetSourceUrl { get; } - public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) + public UrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) { this.assetStore = assetStore; this.urlsOptions = urlsOptions.Value; diff --git a/src/Squidex.Web/Squidex.Web.csproj b/src/Squidex.Web/Squidex.Web.csproj new file mode 100644 index 000000000..bbe645f13 --- /dev/null +++ b/src/Squidex.Web/Squidex.Web.csproj @@ -0,0 +1,23 @@ + + + netstandard2.0 + 7.3 + + + full + True + + + + + + + + + + ..\..\Squidex.ruleset + + + + + diff --git a/src/Squidex.Web/SquidexWeb.cs b/src/Squidex.Web/SquidexWeb.cs new file mode 100644 index 000000000..ea1ea613b --- /dev/null +++ b/src/Squidex.Web/SquidexWeb.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Reflection; + +#pragma warning disable RECS0014 // If all fields, properties and methods members are static, the class can be made static. + +namespace Squidex.Web +{ + public sealed class SquidexWeb + { + public static readonly Assembly Assembly = typeof(SquidexWeb).Assembly; + } +} diff --git a/src/Squidex/Config/MyUrlsOptions.cs b/src/Squidex.Web/UrlsOptions.cs similarity index 93% rename from src/Squidex/Config/MyUrlsOptions.cs rename to src/Squidex.Web/UrlsOptions.cs index 32e4022c0..b45390301 100644 --- a/src/Squidex/Config/MyUrlsOptions.cs +++ b/src/Squidex.Web/UrlsOptions.cs @@ -7,9 +7,9 @@ using Squidex.Infrastructure; -namespace Squidex.Config +namespace Squidex.Web { - public sealed class MyUrlsOptions + public sealed class UrlsOptions { public bool EnforceHTTPS { get; set; } diff --git a/src/Squidex/Config/MyUsageOptions.cs b/src/Squidex.Web/UsageOptions.cs similarity index 89% rename from src/Squidex/Config/MyUsageOptions.cs rename to src/Squidex.Web/UsageOptions.cs index 339c665bc..c9502e38e 100644 --- a/src/Squidex/Config/MyUsageOptions.cs +++ b/src/Squidex.Web/UsageOptions.cs @@ -7,9 +7,9 @@ using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -namespace Squidex.Config +namespace Squidex.Web { - public class MyUsageOptions + public sealed class UsageOptions { public ConfigAppLimitsPlan[] Plans { get; set; } } diff --git a/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs index e0d01097e..9bd4f0b70 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/ScopesProcessor.cs @@ -13,9 +13,8 @@ using Microsoft.AspNetCore.Authorization; using NSwag; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; -using Squidex.Config; using Squidex.Infrastructure.Tasks; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Config.Swagger { diff --git a/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs index 9133a2e86..2b5b5b24c 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SecurityProcessor.cs @@ -9,19 +9,19 @@ using System.Collections.Generic; using Microsoft.Extensions.Options; using NSwag; using NSwag.SwaggerGeneration.Processors.Security; -using Squidex.Config; using Squidex.Pipeline.Swagger; +using Squidex.Web; namespace Squidex.Areas.Api.Config.Swagger { public class SecurityProcessor : SecurityDefinitionAppender { - public SecurityProcessor(IOptions urlOptions) + public SecurityProcessor(IOptions urlOptions) : base(Constants.SecurityDefinition, CreateOAuthSchema(urlOptions.Value)) { } - private static SwaggerSecurityScheme CreateOAuthSchema(MyUrlsOptions urlOptions) + private static SwaggerSecurityScheme CreateOAuthSchema(UrlsOptions urlOptions) { var securityScheme = new SwaggerSecurityScheme(); diff --git a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs index 5059cb21c..ddccd616c 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/SwaggerServices.cs @@ -14,7 +14,6 @@ using NSwag.SwaggerGeneration; using NSwag.SwaggerGeneration.Processors; using Squidex.Areas.Api.Controllers.Contents.Generator; using Squidex.Areas.Api.Controllers.Rules.Models; -using Squidex.Config; using Squidex.Infrastructure; namespace Squidex.Areas.Api.Config.Swagger diff --git a/src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs index b69e54972..bc5106af9 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/ThemeProcessor.cs @@ -10,8 +10,8 @@ using System.Threading.Tasks; using Microsoft.Extensions.Options; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; -using Squidex.Config; using Squidex.Infrastructure.Tasks; +using Squidex.Web; namespace Squidex.Areas.Api.Config.Swagger { @@ -21,7 +21,7 @@ namespace Squidex.Areas.Api.Config.Swagger private readonly string url; - public ThemeProcessor(IOptions urlOptions) + public ThemeProcessor(IOptions urlOptions) { url = urlOptions.Value.BuildUrl("images/logo-white.png", false); } diff --git a/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs b/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs index 01dc1c73b..b0ecdf36d 100644 --- a/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs +++ b/src/Squidex/Areas/Api/Config/Swagger/XmlTagProcessor.cs @@ -22,7 +22,7 @@ namespace Squidex.Areas.Api.Config.Swagger { foreach (var controllerType in context.ControllerTypes) { - var attribute = controllerType.GetTypeInfo().GetCustomAttribute(); + var attribute = controllerType.GetCustomAttribute(); if (attribute != null) { diff --git a/src/Squidex/Areas/Api/Controllers/ApiController.cs b/src/Squidex/Areas/Api/Controllers/ApiController.cs index e991604c3..03ccc89c1 100644 --- a/src/Squidex/Areas/Api/Controllers/ApiController.cs +++ b/src/Squidex/Areas/Api/Controllers/ApiController.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 085170779..f60427864 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -12,8 +12,8 @@ using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 571d9be66..d8af9f53b 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 4165652f7..17ce8c320 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -13,8 +13,8 @@ using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs index a31267774..29783a09e 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs @@ -13,8 +13,8 @@ using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index a8aacc1ba..e146c90b3 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs @@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index fb765658c..4219f42dd 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -16,9 +16,9 @@ using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; -using Squidex.Pipeline; using Squidex.Shared; using Squidex.Shared.Identity; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index e2245fb41..7fe96d7bc 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -14,8 +14,8 @@ using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Apps.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 9d7b7ce3e..cf71326c0 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Log; -using Squidex.Pipeline; +using Squidex.Web; #pragma warning disable 1573 diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index d467d6543..0d00940d3 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -23,8 +23,8 @@ using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Assets { diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index e0ce409c5..733a3ab18 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -12,7 +12,7 @@ using NodaTime; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Assets.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index ba2dbe410..25c965033 100644 --- a/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Backups { diff --git a/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs index 3c433fbdb..0198fa186 100644 --- a/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs @@ -15,8 +15,8 @@ using Squidex.Domain.Apps.Entities.Backup; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Tasks; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Backups { diff --git a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs index d564e8d11..6ad1cf1c0 100644 --- a/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs +++ b/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs @@ -13,8 +13,8 @@ using Squidex.Domain.Apps.Entities.Backup; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Security; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Backups { diff --git a/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index 281c63f67..5603ec207 100644 --- a/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -15,8 +15,8 @@ using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Comments { diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs index aaf2aadf3..c06a178c5 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Contents.Generator; using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents { diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index df849dd56..f5467dfad 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -20,9 +20,9 @@ using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; using Squidex.Shared.Identity; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents { diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs index 6f5812945..7809731b0 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs @@ -10,13 +10,13 @@ using System.Collections.Generic; using System.Linq; using NJsonSchema; using NSwag; -using Squidex.Config; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents.Generator { diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs index 0ab353afb..e2e7a8d2c 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs @@ -18,23 +18,23 @@ using NSwag.SwaggerGeneration; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; using Squidex.Areas.Api.Config.Swagger; -using Squidex.Config; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents.Generator { public sealed class SchemasSwaggerGenerator { - private readonly MyUrlsOptions urlOptions; + private readonly UrlsOptions urlOptions; private readonly SwaggerDocumentSettings settings = new SwaggerDocumentSettings(); private SwaggerJsonSchemaGenerator schemaGenerator; private SwaggerDocument document; private JsonSchemaResolver schemaResolver; - public SchemasSwaggerGenerator(IOptions urlOptions, IEnumerable documentProcessors) + public SchemasSwaggerGenerator(IOptions urlOptions, IEnumerable documentProcessors) { this.urlOptions = urlOptions.Value; diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 12fdd6cb4..5e896298b 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -16,7 +16,7 @@ using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Contents.Models { diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index f8a2c9d0d..1369eb9c7 100644 --- a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs +++ b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -13,8 +13,8 @@ using Squidex.Areas.Api.Controllers.EventConsumers.Models; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.Orleans; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.EventConsumers { diff --git a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index e09a2aa94..bb5b5fc57 100644 --- a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -11,8 +11,8 @@ using Squidex.Areas.Api.Controllers.History.Models; using Squidex.Domain.Apps.Entities.History; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.History { diff --git a/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs index 77ca6fed2..85f5c5133 100644 --- a/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Languages { diff --git a/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/src/Squidex/Areas/Api/Controllers/News/NewsController.cs index e80dc19b7..f163227f0 100644 --- a/src/Squidex/Areas/Api/Controllers/News/NewsController.cs +++ b/src/Squidex/Areas/Api/Controllers/News/NewsController.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.News.Models; using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.News { diff --git a/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs index c27e14477..22a0254ad 100644 --- a/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs +++ b/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs @@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Ping { diff --git a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 5b5a8fe65..9490a4f48 100644 --- a/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -11,8 +11,8 @@ using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Plans.Models; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Plans { 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 2b9bd6328..e0d44e0e4 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -33,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters public RuleTriggerDto Visit(SchemaChangedTrigger trigger) { - return SimpleMapper.Map(trigger, new AssetChangedRuleTriggerDto()); + return SimpleMapper.Map(trigger, new SchemaChangedRuleTriggerDto()); } public RuleTriggerDto Visit(UsageTrigger trigger) diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs index 4efcfb7ad..8f1da7b9e 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionConverter.cs @@ -7,15 +7,14 @@ using System; using System.Collections.Generic; -using System.Linq; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Extensions.Actions; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules.Models { public sealed class RuleActionConverter : MyJsonInheritanceConverter { - private static readonly Dictionary Mapping = RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => x.Value.Type); + public static IReadOnlyDictionary Mapping { get; set; } public RuleActionConverter() : base("actionType", Mapping) diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs index a0d23e12b..b0337ed42 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleActionProcessor.cs @@ -11,13 +11,23 @@ using System.Threading.Tasks; using NJsonSchema; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Extensions.Actions; +using Squidex.Infrastructure; namespace Squidex.Areas.Api.Controllers.Rules.Models { public sealed class RuleActionProcessor : IDocumentProcessor { + private readonly RuleRegistry ruleRegistry; + + public RuleActionProcessor(RuleRegistry ruleRegistry) + { + Guard.NotNull(ruleRegistry, nameof(ruleRegistry)); + + this.ruleRegistry = ruleRegistry; + } + public async Task ProcessAsync(DocumentProcessorContext context) { try @@ -36,16 +46,16 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models Type = JsonObjectType.String, IsRequired = true }; - foreach (var derived in RuleElementRegistry.Actions) + foreach (var action in ruleRegistry.Actions) { - var derivedSchema = await context.SchemaGenerator.GenerateAsync(derived.Value.Type, context.SchemaResolver); + var derivedSchema = await context.SchemaGenerator.GenerateAsync(action.Value.Type, context.SchemaResolver); var oldName = context.Document.Definitions.FirstOrDefault(x => x.Value == derivedSchema).Key; if (oldName != null) { context.Document.Definitions.Remove(oldName); - context.Document.Definitions.Add(derived.Key, derivedSchema); + context.Document.Definitions.Add(action.Key, derivedSchema); } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs index a782c9e2d..4ee256530 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs @@ -14,7 +14,7 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs index e419be526..910e87c38 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementDto.cs @@ -6,6 +6,9 @@ // ========================================================================== using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Rules.Models { @@ -37,5 +40,20 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models /// The optional link to the product that is integrated. /// public string ReadMore { get; set; } + + /// + /// The properties. + /// + [Required] + public RuleElementPropertyDto[] Properties { get; set; } + + public static RuleElementDto FromDefinition(RuleActionDefinition definition) + { + var result = SimpleMapper.Map(definition, new RuleElementDto()); + + result.Properties = definition.Properties.Select(x => SimpleMapper.Map(x, new RuleElementPropertyDto())).ToArray(); + + return result; + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs new file mode 100644 index 000000000..ea9c72a64 --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleElementPropertyDto.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.HandleRules; + +namespace Squidex.Areas.Api.Controllers.Rules.Models +{ + public sealed class RuleElementPropertyDto + { + /// + /// The html editor. + /// + [Required] + public RuleActionPropertyEditor Editor { get; set; } + + /// + /// The name of the editor. + /// + [Required] + public string Name { get; set; } + + /// + /// The label to use. + /// + [Required] + public string Display { get; set; } + + /// + /// The optional description. + /// + public string Description { get; set; } + + /// + /// Indicates if the property is formattable. + /// + public bool IsFormattable { get; set; } + + /// + /// Indicates if the property is required. + /// + public bool IsRequired { get; set; } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs index 7671db829..9ac6cd699 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/RuleTriggerDto.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 998f08a70..b657ef1cf 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -13,15 +13,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using NodaTime; using Squidex.Areas.Api.Controllers.Rules.Models; +using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Extensions.Actions; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Reflection; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Rules { @@ -31,18 +30,18 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiExplorerSettings(GroupName = nameof(Rules))] public sealed class RulesController : ApiController { - private static readonly string RuleActionsEtag = string.Join(";", RuleElementRegistry.Actions.Select(x => x.Key)).Sha256Base64(); - private static readonly string RuleTriggersEtag = string.Join(";", RuleElementRegistry.Triggers.Select(x => x.Key)).Sha256Base64(); private readonly IAppProvider appProvider; private readonly IRuleEventRepository ruleEventsRepository; + private readonly RuleRegistry ruleRegistry; public RulesController(ICommandBus commandBus, IAppProvider appProvider, - IRuleEventRepository ruleEventsRepository) + IRuleEventRepository ruleEventsRepository, RuleRegistry ruleRegistry) : base(commandBus) { this.appProvider = appProvider; this.ruleEventsRepository = ruleEventsRepository; + this.ruleRegistry = ruleRegistry; } /// @@ -58,29 +57,11 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(0)] public IActionResult GetActions() { - var response = RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto())); + var etag = string.Join(";", ruleRegistry.Actions.Select(x => x.Key)).Sha256Base64(); - Response.Headers[HeaderNames.ETag] = RuleActionsEtag; + var response = ruleRegistry.Actions.ToDictionary(x => x.Key, x => RuleElementDto.FromDefinition(x.Value)); - return Ok(response); - } - - /// - /// Get the supported rule triggers. - /// - /// - /// 200 => Rule triggers returned. - /// - [HttpGet] - [Route("rules/triggers/")] - [ProducesResponseType(typeof(Dictionary), 200)] - [ApiPermission] - [ApiCosts(0)] - public IActionResult GetTriggers() - { - var response = RuleElementRegistry.Triggers.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto())); - - Response.Headers[HeaderNames.ETag] = RuleTriggersEtag; + Response.Headers[HeaderNames.ETag] = etag; return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs b/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs deleted file mode 100644 index 54e9e1566..000000000 --- a/src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using CoreTweet; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Squidex.Extensions.Actions.Twitter; - -namespace Squidex.Areas.Api.Controllers.Rules -{ - public sealed class TwitterController : Controller - { - private readonly TwitterOptions twitterOptions; - - public TwitterController(IOptions twitterOptions) - { - this.twitterOptions = twitterOptions.Value; - } - - public sealed class TokenRequest - { - public string PinCode { get; set; } - - public string RequestToken { get; set; } - - public string RequestTokenSecret { get; set; } - } - - [HttpGet] - [Route("rules/twitter/auth")] - public async Task Auth() - { - var session = await OAuth.AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret); - - return Ok(new - { - session.AuthorizeUri, - session.RequestToken, - session.RequestTokenSecret - }); - } - - [HttpPost] - [Route("rules/twitter/token")] - public async Task AuthComplete([FromBody] TokenRequest request) - { - var session = new OAuth.OAuthSession - { - ConsumerKey = twitterOptions.ClientId, - ConsumerSecret = twitterOptions.ClientSecret, - RequestToken = request.RequestToken, - RequestTokenSecret = request.RequestTokenSecret - }; - - var tokens = await session.GetTokensAsync(request.PinCode); - - return Ok(new - { - tokens.AccessToken, - tokens.AccessTokenSecret - }); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs index afb21c6b9..02376143b 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Schemas.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs index 73794552f..f66d64d23 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs @@ -11,7 +11,7 @@ using NodaTime; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Schemas.Models { diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 048607c97..74a36f916 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -10,8 +10,8 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Schemas { diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 777495ffd..1fb64ec83 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -15,8 +15,8 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Schemas { diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 86287b9b0..d7a7f4eaa 100644 --- a/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -13,14 +13,13 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Squidex.Areas.Api.Controllers.Statistics.Models; -using Squidex.Config; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.UsageTracking; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Statistics { @@ -35,7 +34,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics private readonly IAppPlansProvider appPlansProvider; private readonly IAssetUsageTracker assetStatsRepository; private readonly IDataProtector dataProtector; - private readonly MyUrlsOptions urlsOptions; + private readonly UrlsOptions urlsOptions; public UsagesController( ICommandBus commandBus, @@ -44,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics IAppPlansProvider appPlansProvider, IAssetUsageTracker assetStatsRepository, IDataProtectionProvider dataProtection, - IOptions urlsOptions) + IOptions urlsOptions) : base(commandBus) { this.usageTracker = usageTracker; diff --git a/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index e5fbe040e..5fc5cd8e4 100644 --- a/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -10,8 +10,8 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Translations.Models; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Translations; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Translations { diff --git a/src/Squidex/Config/MyUIOptions.cs b/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs similarity index 95% rename from src/Squidex/Config/MyUIOptions.cs rename to src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs index 5ac51ece4..5d87d6df8 100644 --- a/src/Squidex/Config/MyUIOptions.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; -namespace Squidex.Config +namespace Squidex.Areas.Api.Controllers.UI { public sealed class MyUIOptions { diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 18a7eeb7e..1de619114 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -10,30 +10,26 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Orleans; using Squidex.Areas.Api.Controllers.UI.Models; -using Squidex.Config; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Extensions.Actions.Twitter; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Orleans; -using Squidex.Pipeline; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.UI { public sealed class UIController : ApiController { private readonly MyUIOptions uiOptions; - private readonly TwitterOptions twitterOptions; private readonly IGrainFactory grainFactory; public UIController(ICommandBus commandBus, IOptions uiOptions, - IOptions twitterOptions, IGrainFactory grainFactory) : base(commandBus) { this.uiOptions = uiOptions.Value; + this.grainFactory = grainFactory; - this.twitterOptions = twitterOptions.Value; } /// @@ -55,8 +51,6 @@ namespace Squidex.Areas.Api.Controllers.UI result.Value.Add("mapType", uiOptions.Map?.Type ?? "OSM"); result.Value.Add("mapKey", uiOptions.Map?.GoogleMaps?.Key); - result.Value.Add("supportTwitterAction", twitterOptions.IsConfigured()); - return Ok(result.Value); } diff --git a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index ef7e8cb65..7d547be14 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -15,8 +15,8 @@ using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; -using Squidex.Pipeline; using Squidex.Shared; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Users { diff --git a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 3aef01d84..406510caf 100644 --- a/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -9,15 +9,14 @@ using System; using System.IO; using System.Linq; using System.Net.Http; -using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Users.Models; using Squidex.Domain.Users; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Log; -using Squidex.Pipeline; using Squidex.Shared.Users; +using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Users { @@ -34,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Users static UsersController() { - var assembly = typeof(UsersController).GetTypeInfo().Assembly; + var assembly = typeof(UsersController).Assembly; using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) { diff --git a/src/Squidex/Areas/Api/Startup.cs b/src/Squidex/Areas/Api/Startup.cs index 5d2cefaa6..55e2383fc 100644 --- a/src/Squidex/Areas/Api/Startup.cs +++ b/src/Squidex/Areas/Api/Startup.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Builder; using Squidex.Areas.Api.Config.Swagger; -using Squidex.Config; +using Squidex.Web; namespace Squidex.Areas.Api { diff --git a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index f5025efd6..8ede8a1bb 100644 --- a/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ b/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Collections.Generic; -using System.Reflection; using System.Security.Cryptography.X509Certificates; using IdentityModel; using IdentityServer4.Models; @@ -17,9 +16,9 @@ using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Squidex.Config; using Squidex.Domain.Users; using Squidex.Shared.Identity; +using Squidex.Web; namespace Squidex.Areas.IdentityServer.Config { @@ -29,7 +28,7 @@ namespace Squidex.Areas.IdentityServer.Config { X509Certificate2 certificate; - var assembly = typeof(IdentityServerServices).GetTypeInfo().Assembly; + var assembly = typeof(IdentityServerServices).Assembly; using (var certStream = assembly.GetManifestResourceStream("Squidex.Areas.IdentityServer.Config.Cert.IdentityCert.pfx")) { diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs index af60dc5e3..c4b619466 100644 --- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -17,9 +17,9 @@ using Squidex.Config; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; -using Squidex.Pipeline; using Squidex.Shared; using Squidex.Shared.Identity; +using Squidex.Web; namespace Squidex.Areas.IdentityServer.Config { @@ -29,7 +29,7 @@ namespace Squidex.Areas.IdentityServer.Config private readonly Dictionary staticClients = new Dictionary(StringComparer.OrdinalIgnoreCase); public LazyClientStore( - IOptions urlsOptions, + IOptions urlsOptions, IOptions identityOptions, IAppProvider appProvider) { @@ -89,7 +89,7 @@ namespace Squidex.Areas.IdentityServer.Config }; } - private void CreateStaticClients(IOptions urlsOptions, IOptions identityOptions) + private void CreateStaticClients(IOptions urlsOptions, IOptions identityOptions) { foreach (var client in CreateStaticClients(urlsOptions.Value, identityOptions.Value)) { @@ -97,7 +97,7 @@ namespace Squidex.Areas.IdentityServer.Config } } - private static IEnumerable CreateStaticClients(MyUrlsOptions urlsOptions, MyIdentityOptions identityOptions) + private static IEnumerable CreateStaticClients(UrlsOptions urlsOptions, MyIdentityOptions identityOptions) { var frontendId = Constants.FrontendClient; diff --git a/src/Squidex/Areas/IdentityServer/Startup.cs b/src/Squidex/Areas/IdentityServer/Startup.cs index e98f0aff3..d46ab509e 100644 --- a/src/Squidex/Areas/IdentityServer/Startup.cs +++ b/src/Squidex/Areas/IdentityServer/Startup.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Squidex.Areas.IdentityServer.Config; -using Squidex.Config; +using Squidex.Web; namespace Squidex.Areas.IdentityServer { diff --git a/src/Squidex/Areas/OrleansDashboard/Startup.cs b/src/Squidex/Areas/OrleansDashboard/Startup.cs index d8b5da6f8..943057450 100644 --- a/src/Squidex/Areas/OrleansDashboard/Startup.cs +++ b/src/Squidex/Areas/OrleansDashboard/Startup.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Builder; using Orleans; using Squidex.Areas.OrleansDashboard.Middlewares; -using Squidex.Config; +using Squidex.Web; namespace Squidex.Areas.OrleansDashboard { diff --git a/src/Squidex/Areas/Portal/Startup.cs b/src/Squidex/Areas/Portal/Startup.cs index 70490676a..88cc7646b 100644 --- a/src/Squidex/Areas/Portal/Startup.cs +++ b/src/Squidex/Areas/Portal/Startup.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Builder; using Squidex.Areas.Portal.Middlewares; -using Squidex.Config; +using Squidex.Web; namespace Squidex.Areas.Portal { diff --git a/src/Squidex/Config/Authentication/IdentityServerServices.cs b/src/Squidex/Config/Authentication/IdentityServerServices.cs index bcf0b9916..deb1a9ad1 100644 --- a/src/Squidex/Config/Authentication/IdentityServerServices.cs +++ b/src/Squidex/Config/Authentication/IdentityServerServices.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure; +using Squidex.Web; namespace Squidex.Config.Authentication { @@ -20,7 +21,7 @@ namespace Squidex.Config.Authentication { var apiScope = Constants.ApiScope; - var urlsOptions = config.GetSection("urls").Get(); + var urlsOptions = config.GetSection("urls").Get(); if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl)) { diff --git a/src/Squidex/Config/Authentication/OidcServices.cs b/src/Squidex/Config/Authentication/OidcServices.cs index 5d34ee58d..de3eebe6a 100644 --- a/src/Squidex/Config/Authentication/OidcServices.cs +++ b/src/Squidex/Config/Authentication/OidcServices.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.DependencyInjection; +using Squidex.Web; namespace Squidex.Config.Authentication { diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs index 8ec712b17..669cd29b8 100644 --- a/src/Squidex/Config/Domain/AssetServices.cs +++ b/src/Squidex/Config/Domain/AssetServices.cs @@ -20,21 +20,26 @@ namespace Squidex.Config.Domain { public static void AddMyAssetServices(this IServiceCollection services, IConfiguration config) { - config.ConfigureByOption("assetStore:type", new Options + config.ConfigureByOption("assetStore:type", new Alternatives { + ["Default"] = () => + { + services.AddSingletonAs() + .AsOptional(); + }, ["Folder"] = () => { var path = config.GetRequiredValue("assetStore:folder:path"); services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService())) - .As(); + .AsOptional(); }, ["GoogleCloud"] = () => { var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket"); services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName)) - .As(); + .AsOptional(); }, ["AzureBlob"] = () => { @@ -42,7 +47,7 @@ namespace Squidex.Config.Domain var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName"); services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName)) - .As(); + .AsOptional(); }, ["MongoDb"] = () => { @@ -62,7 +67,7 @@ namespace Squidex.Config.Domain return new MongoGridFsAssetStore(gridFsbucket); }) - .As(); + .AsOptional(); } }); diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 8a8b554a7..b1deb7ae3 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Options; using Migrate_01; using Migrate_01.Migrations; using Orleans; +using Squidex.Areas.Api.Controllers.UI; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.HandleRules; @@ -31,6 +32,7 @@ using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Edm; using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; @@ -44,8 +46,10 @@ using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; -using Squidex.Pipeline; -using Squidex.Pipeline.CommandMiddlewares; +using Squidex.Infrastructure.Orleans; +using Squidex.Web; +using Squidex.Web.CommandMiddlewares; +using Squidex.Web.Services; namespace Squidex.Config.Domain { @@ -56,18 +60,16 @@ namespace Squidex.Config.Domain var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); services.AddSingletonAs(c => new UrlGenerator( - c.GetRequiredService>(), + c.GetRequiredService>(), c.GetRequiredService(), exposeSourceUrl)) .As().As().As(); services.AddSingletonAs() - .As() - .As(); + .As().As(); services.AddSingletonAs() - .As() - .As(); + .As().As(); services.AddSingletonAs() .As(); @@ -105,6 +107,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As>(); @@ -112,7 +117,13 @@ namespace Squidex.Config.Domain .As>(); services.AddSingletonAs() - .As(); + .AsOptional(); + + services.AddSingletonAs>() + .AsSelf(); + + services.AddSingletonAs>() + .AsSelf(); services.AddCommandPipeline(); services.AddBackupHandlers(); @@ -248,6 +259,9 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); + services.AddTransientAs() + .As(); + services.AddTransientAs() .As(); diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs index defcbc2c7..ee90b1d47 100644 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ b/src/Squidex/Config/Domain/EventStoreServices.cs @@ -5,11 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Linq; using EventStore.ClientAPI; +using Microsoft.Azure.Documents.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; +using Newtonsoft.Json; using Squidex.Infrastructure; using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.EventSourcing; @@ -23,7 +26,7 @@ namespace Squidex.Config.Domain { public static void AddMyEventStoreServices(this IServiceCollection services, IConfiguration config) { - config.ConfigureByOption("eventStore:type", new Options + config.ConfigureByOption("eventStore:type", new Alternatives { ["MongoDb"] = () => { @@ -37,7 +40,26 @@ namespace Squidex.Config.Domain return new MongoEventStore(mongDatabase, c.GetRequiredService()); }) - .As(); + .AsOptional(); + }, + ["CosmosDb"] = () => + { + var cosmosDbConfiguration = config.GetRequiredValue("eventStore:cosmosDB:configuration"); + var cosmosDbMasterKey = config.GetRequiredValue("eventStore:cosmosDB:masterKey"); + var cosmosDbDatabase = config.GetRequiredValue("eventStore:cosmosDB:database"); + + services.AddSingletonAs(c => new DocumentClient(new Uri(cosmosDbConfiguration), cosmosDbMasterKey, c.GetRequiredService())) + .AsSelf(); + + services.AddSingletonAs(c => new CosmosDbEventStore( + c.GetRequiredService(), + cosmosDbMasterKey, + cosmosDbDatabase, + c.GetRequiredService())) + .AsOptional(); + + services.AddHealthChecks() + .AddCheck("CosmosDB", tags: new[] { "node" }); }, ["GetEventStore"] = () => { @@ -45,13 +67,15 @@ namespace Squidex.Config.Domain var eventStoreProjectionHost = config.GetRequiredValue("eventStore:getEventStore:projectionHost"); var eventStorePrefix = config.GetValue("eventStore:getEventStore:prefix"); - var connection = EventStoreConnection.Create(eventStoreConfiguration); - - services.AddSingletonAs(connection) + services.AddSingletonAs(_ => EventStoreConnection.Create(eventStoreConfiguration)) .As(); - services.AddSingletonAs(c => new GetEventStore(connection, c.GetRequiredService(), eventStorePrefix, eventStoreProjectionHost)) - .As(); + services.AddSingletonAs(c => new GetEventStore( + c.GetRequiredService(), + c.GetRequiredService(), + eventStorePrefix, + eventStoreProjectionHost)) + .AsOptional(); services.AddHealthChecks() .AddCheck("EventStore", tags: new[] { "node" }); diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 33687222d..915dd035a 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -15,11 +14,12 @@ using Microsoft.Extensions.DependencyInjection; using NodaTime; using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Domain.Apps.Entities.Apps.Diagnostics; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Users; -using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.UsageTracking; using Squidex.Shared.Users; @@ -32,25 +32,17 @@ namespace Squidex.Config.Domain { public static void AddMyInfrastructureServices(this IServiceCollection services, IConfiguration config) { - var deeplAuthKey = config.GetValue("translations:deeplAuthKey"); - - if (!string.IsNullOrWhiteSpace(deeplAuthKey)) - { - services.AddSingletonAs(c => new DeepLTranslator(deeplAuthKey, c.GetRequiredService())) - .As(); - } - else - { - services.AddSingletonAs() - .As(); - } - services.AddHealthChecks() .AddCheck("GC", tags: new[] { "node" }) .AddCheck("Orleans", tags: new[] { "cluster" }) .AddCheck("Orleans App", tags: new[] { "cluster" }); - services.AddSingletonAs(SystemClock.Instance) + services.AddSingletonAs(c => new CachingUsageTracker( + c.GetRequiredService(), + c.GetRequiredService())) + .As(); + + services.AddSingletonAs(_ => SystemClock.Instance) .As(); services.AddSingletonAs() @@ -59,8 +51,14 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs(c => new CachingUsageTracker(c.GetRequiredService(), c.GetRequiredService())) - .As(); + services.AddSingletonAs>() + .AsSelf(); + + services.AddSingletonAs>() + .AsSelf(); + + services.AddSingletonAs() + .As(); services.AddSingletonAs() .As(); @@ -72,15 +70,13 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As(); + .AsOptional(); services.AddSingletonAs() - .As(); + .AsOptional(); services.AddSingletonAs() .As(); - - services.AddTransient(typeof(Lazy<>), typeof(Lazier<>)); } } } diff --git a/src/Squidex/Config/Domain/LoggingServices.cs b/src/Squidex/Config/Domain/LoggingServices.cs index 5e18bd491..f78858a8c 100644 --- a/src/Squidex/Config/Domain/LoggingServices.cs +++ b/src/Squidex/Config/Domain/LoggingServices.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure.Log; -using Squidex.Pipeline; +using Squidex.Web.Pipeline; namespace Squidex.Config.Domain { @@ -20,12 +20,12 @@ namespace Squidex.Config.Domain { if (config.GetValue("logging:human")) { - services.AddSingletonAs(JsonLogWriterFactory.Readable()) + services.AddSingletonAs(_ => JsonLogWriterFactory.Readable()) .As(); } else { - services.AddSingletonAs(JsonLogWriterFactory.Default()) + services.AddSingletonAs(_ => JsonLogWriterFactory.Default()) .As(); } @@ -33,16 +33,16 @@ namespace Squidex.Config.Domain if (!string.IsNullOrWhiteSpace(loggingFile)) { - services.AddSingletonAs(new FileChannel(loggingFile)) + services.AddSingletonAs(_ => new FileChannel(loggingFile)) .As(); } var useColors = config.GetValue("logging:colors"); - services.AddSingletonAs(new ConsoleLogChannel(useColors)) + services.AddSingletonAs(_ => new ConsoleLogChannel(useColors)) .As(); - services.AddSingletonAs(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) + services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) .As(); services.AddSingletonAs() @@ -61,7 +61,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As(); + .AsOptional(); } } } diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs index 02f36ecc3..eb8ad2cca 100644 --- a/src/Squidex/Config/Domain/RuleServices.cs +++ b/src/Squidex/Config/Domain/RuleServices.cs @@ -12,7 +12,7 @@ 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; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Config.Domain @@ -39,16 +39,14 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As().AsSelf(); + services.AddSingletonAs() .AsSelf(); services.AddSingletonAs() .AsSelf(); - - foreach (var actionHandler in RuleElementRegistry.ActionHandlers) - { - services.AddSingleton(typeof(IRuleActionHandler), actionHandler); - } } } } diff --git a/src/Squidex/Config/Domain/SerializationInitializer.cs b/src/Squidex/Config/Domain/SerializationInitializer.cs new file mode 100644 index 000000000..bdec8c549 --- /dev/null +++ b/src/Squidex/Config/Domain/SerializationInitializer.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Squidex.Areas.Api.Controllers.Rules.Models; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Config.Domain +{ + public sealed class SerializationInitializer : IInitializable + { + private readonly JsonSerializer jsonSerializer; + private readonly RuleRegistry ruleRegistry; + + public SerializationInitializer(JsonSerializer jsonSerializer, RuleRegistry ruleRegistry) + { + this.jsonSerializer = jsonSerializer; + + this.ruleRegistry = ruleRegistry; + } + + public Task InitializeAsync(CancellationToken ct = default) + { + BsonJsonConvention.Register(jsonSerializer); + + RuleActionConverter.Mapping = ruleRegistry.Actions.ToDictionary(x => x.Key, x => x.Value.Type); + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index 04646016c..0b1f7cfce 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -11,12 +11,10 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps.Json; -using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Domain.Apps.Events; -using Squidex.Extensions.Actions; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Newtonsoft; @@ -25,22 +23,9 @@ namespace Squidex.Config.Domain { public static class SerializationServices { - private static readonly TypeNameRegistry TypeNameRegistry = - new TypeNameRegistry() - .MapFields() - .MapRules() - .MapRuleActions() - .MapUnmapped(SquidexCoreModel.Assembly) - .MapUnmapped(SquidexEvents.Assembly) - .MapUnmapped(SquidexInfrastructure.Assembly) - .MapUnmapped(SquidexMigrations.Assembly); - - public static readonly JsonSerializerSettings DefaultJsonSettings = new JsonSerializerSettings(); - public static readonly JsonSerializer DefaultJsonSerializer; - - private static void ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) + private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) { - settings.SerializationBinder = new TypeNameSerializationBinder(TypeNameRegistry); + settings.Converters.Add(new StringEnumConverter()); settings.ContractResolver = new ConverterContractResolver( new AppClientsConverter(), @@ -67,22 +52,52 @@ namespace Squidex.Config.Domain settings.DateParseHandling = DateParseHandling.None; settings.TypeNameHandling = typeNameHandling; - } - - static SerializationServices() - { - ConfigureJson(DefaultJsonSettings, TypeNameHandling.Auto); - DefaultJsonSerializer = JsonSerializer.Create(DefaultJsonSettings); + return settings; } public static IServiceCollection AddMySerializers(this IServiceCollection services) { - services.AddSingleton(DefaultJsonSettings); - services.AddSingleton(DefaultJsonSerializer); - services.AddSingleton(TypeNameRegistry); + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs>() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs(c => JsonSerializer.Create(c.GetRequiredService())) + .AsSelf(); + + services.AddSingletonAs(c => + { + var serializerSettings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.Auto); + + var typeNameRegistry = c.GetService(); + + if (typeNameRegistry != null) + { + serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); + } - services.AddSingleton(new NewtonsoftJsonSerializer(DefaultJsonSettings)); + return serializerSettings; + }).As(); return services; } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 15a41a4d7..218f124a0 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -10,13 +10,14 @@ using IdentityServer4.Stores; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Migrate_01.Migrations; +using Migrate_01.Migrations.MongoDb; using MongoDB.Driver; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Domain.Apps.Entities.MongoDb.Assets; using Squidex.Domain.Apps.Entities.MongoDb.Contents; @@ -41,27 +42,31 @@ namespace Squidex.Config.Domain { public static void AddMyStoreServices(this IServiceCollection services, IConfiguration config) { - config.ConfigureByOption("store:type", new Options + config.ConfigureByOption("store:type", new Alternatives { ["MongoDB"] = () => { - BsonJsonConvention.Register(SerializationServices.DefaultJsonSerializer); - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName); - var mongoClient = Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); - var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); - var mongoContentDatabase = mongoClient.GetDatabase(mongoContentDatabaseName); + var isCosmosDb = config.GetOptionalValue("store:mongoDB:isCosmosDB"); + + services.Configure(config.GetSection("store:mongoDB")); services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>)); - services.AddSingletonAs(mongoDatabase) + services.AddSingletonAs(_ => Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s))) + .As(); + + services.AddSingletonAs(c => c.GetRequiredService().GetDatabase(mongoDatabaseName)) .As(); - services.AddHealthChecks() - .AddCheck("MongoDB", tags: new[] { "node" }); + services.AddTransientAs(c => new DeleteContentCollections(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) + .As(); + + services.AddTransientAs(c => new RestructureContentCollection(c.GetRequiredService().GetDatabase(mongoContentDatabaseName))) + .As(); services.AddSingletonAs() .As(); @@ -72,36 +77,40 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); - services.AddTransientAs(c => new DeleteContentCollections(mongoContentDatabase)) - .As(); + services.AddHealthChecks() + .AddCheck("MongoDB", tags: new[] { "node" }); services.AddSingletonAs() - .As(); + .AsOptional(); services.AddSingletonAs() - .As(); + .AsOptional(); services.AddSingletonAs() - .As(); + .AsOptional(); services.AddSingletonAs() - .As(); + .AsOptional(); services.AddSingletonAs() - .As>(); + .AsOptional>(); services.AddSingletonAs() - .As>() - .As(); + .AsOptional>() + .AsOptional(); services.AddSingletonAs() - .As() - .As>(); - - services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetRequiredService(), c.GetRequiredService())) - .As() - .As>() - .As(); + .AsOptional() + .AsOptional>(); + + services.AddSingletonAs(c => new MongoContentRepository( + c.GetRequiredService().GetDatabase(mongoContentDatabaseName), + c.GetRequiredService(), + c.GetRequiredService(), + c.GetRequiredService())) + .AsOptional() + .AsOptional>() + .AsOptional(); } }); diff --git a/src/Squidex/Config/Domain/SubscriptionServices.cs b/src/Squidex/Config/Domain/SubscriptionServices.cs index 10437c2b7..82e370e7b 100644 --- a/src/Squidex/Config/Domain/SubscriptionServices.cs +++ b/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; using Squidex.Domain.Users; using Squidex.Infrastructure; +using Squidex.Web; namespace Squidex.Config.Domain { @@ -19,16 +20,16 @@ namespace Squidex.Config.Domain { public static void AddMySubscriptionServices(this IServiceCollection services, IConfiguration config) { - services.AddSingletonAs(c => c.GetRequiredService>()?.Value?.Plans.OrEmpty()); + services.AddSingletonAs(c => c.GetRequiredService>()?.Value?.Plans.OrEmpty()); services.AddSingletonAs() - .As(); + .AsOptional(); services.AddSingletonAs() - .As(); + .AsOptional(); services.AddSingletonAs() - .As(); + .AsOptional(); } } } diff --git a/src/Squidex/Config/Orleans/Extensions.cs b/src/Squidex/Config/Orleans/Extensions.cs deleted file mode 100644 index 4ecbd022f..000000000 --- a/src/Squidex/Config/Orleans/Extensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Orleans; -using Orleans.ApplicationParts; -using Orleans.Configuration; -using Orleans.Hosting; -using OrleansDashboard; -using OrleansDashboard.Client; -using OrleansDashboard.Metrics; -using Squidex.Domain.Apps.Entities; -using Squidex.Infrastructure; - -namespace Squidex.Config.Orleans -{ - public static class Extensions - { - public static void AddMyParts(this IApplicationPartManager builder) - { - builder.AddApplicationPart(SquidexEntities.Assembly); - builder.AddApplicationPart(SquidexInfrastructure.Assembly); - } - - public static void Configure(this ClusterOptions options) - { - options.ClusterId = Constants.OrleansClusterId; - options.ServiceId = Constants.OrleansClusterId; - } - - public static ISiloHostBuilder UseDashboardEx(this ISiloHostBuilder builder, Action configurator = null) - { - builder.AddStartupTask(); - - builder.ConfigureApplicationParts(appParts => - appParts - .AddFrameworkPart(typeof(Dashboard).Assembly) - .AddFrameworkPart(typeof(DashboardClient).Assembly)); - - builder.ConfigureServices(services => - { - services.AddDashboard(options => - { - options.HostSelf = false; - }); - }); - - builder.AddIncomingGrainCallFilter(); - - return builder; - } - } -} diff --git a/src/Squidex/Config/Orleans/OrleansServices.cs b/src/Squidex/Config/Orleans/OrleansServices.cs index aed630ded..d30ed84a0 100644 --- a/src/Squidex/Config/Orleans/OrleansServices.cs +++ b/src/Squidex/Config/Orleans/OrleansServices.cs @@ -5,115 +5,109 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Net; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Orleans; using Orleans.Configuration; using Orleans.Hosting; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; -using Squidex.Infrastructure.EventSourcing.Grains; +using OrleansDashboard; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans; +using Squidex.Web; +using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; namespace Squidex.Config.Orleans { public static class OrleansServices { - public static IServiceProvider AddAndBuildOrleans(this IServiceCollection services, IConfiguration config, Action afterServices) + public static IServiceCollection AddOrleans(this IServiceCollection services, IConfiguration config, IWebHostEnvironment environment) { - services.Configure(options => + services.AddOrleans(config, environment, builder => { - options.Configure(); - }); + builder.ConfigureServices(siloServices => + { + siloServices.Configure(options => + { + options.Configure(); + }); - services.Configure(options => - { - options.FastKillOnProcessExit = false; - }); + siloServices.Configure(options => + { + options.FastKillOnProcessExit = false; + }); - services.AddServicesForSelfHostedDashboard(null, options => - { - options.HideTrace = true; - }); + siloServices.Configure(options => + { + options.HideTrace = true; + }); - services.AddHostedService(); - - var hostBuilder = new SiloHostBuilder() - .UseDashboardEx() - .EnableDirectClient() - .AddIncomingGrainCallFilter() - .AddStartupTask>() - .AddStartupTask>() - .AddStartupTask>() - .AddStartupTask>() - .ConfigureApplicationParts(builder => - { - builder.AddMyParts(); + siloServices.AddSingleton(); }); - var gatewayPort = config.GetOptionalValue("orleans:gatewayPort", 40000); - - var siloPort = config.GetOptionalValue("orleans:siloPort", 11111); + builder.ConfigureApplicationParts(parts => + { + parts.AddApplicationPart(SquidexEntities.Assembly); + parts.AddApplicationPart(SquidexInfrastructure.Assembly); + }); - config.ConfigureByOption("orleans:clustering", new Options - { - ["MongoDB"] = () => + builder.UseDashboard(options => { - hostBuilder.ConfigureEndpoints(Dns.GetHostName(), siloPort, gatewayPort, listenOnAnyHostAddress: true); + options.HostSelf = false; + }); - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + var gatewayPort = config.GetOptionalValue("orleans:gatewayPort", 40000); - hostBuilder.UseMongoDBClustering(options => - { - options.ConnectionString = mongoConfiguration; - options.CollectionPrefix = "Orleans_"; - options.DatabaseName = mongoDatabaseName; - }); - }, - ["Development"] = () => - { - hostBuilder.UseLocalhostClustering(siloPort, gatewayPort, null, Constants.OrleansClusterId, Constants.OrleansClusterId); - hostBuilder.Configure(options => options.ExpectedClusterSize = 1); - } - }); + var siloPort = config.GetOptionalValue("orleans:siloPort", 11111); - config.ConfigureByOption("store:type", new Options - { - ["MongoDB"] = () => + config.ConfigureByOption("orleans:clustering", new Alternatives { - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - - hostBuilder.UseMongoDBReminders(options => + ["MongoDB"] = () => { - options.ConnectionString = mongoConfiguration; - options.CollectionPrefix = "Orleans_"; - options.DatabaseName = mongoDatabaseName; - }); - } - }); - - IServiceProvider provider = null; + builder.ConfigureEndpoints(Dns.GetHostName(), siloPort, gatewayPort, listenOnAnyHostAddress: true); + + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + + builder.UseMongoDBClustering(options => + { + options.ConnectionString = mongoConfiguration; + options.CollectionPrefix = "Orleans_"; + options.DatabaseName = mongoDatabaseName; + }); + }, + ["Development"] = () => + { + builder.UseLocalhostClustering(siloPort, gatewayPort, null, Constants.OrleansClusterId, Constants.OrleansClusterId); + builder.Configure(options => options.ExpectedClusterSize = 1); + } + }); - hostBuilder.UseServiceProviderFactory((siloServices) => - { - foreach (var descriptor in services) + config.ConfigureByOption("store:type", new Alternatives { - siloServices.Add(descriptor); - } - - afterServices(siloServices); - - provider = siloServices.BuildServiceProvider(); + ["MongoDB"] = () => + { + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + + builder.UseMongoDBReminders(options => + { + options.ConnectionString = mongoConfiguration; + options.CollectionPrefix = "Orleans_"; + options.DatabaseName = mongoDatabaseName; + }); + } + }); + }); - return provider; - }).Build(); + return services; + } - return provider; + public static void Configure(this ClusterOptions options) + { + options.ClusterId = Constants.OrleansClusterId; + options.ServiceId = Constants.OrleansClusterId; } } } diff --git a/src/Squidex/Config/Orleans/SiloHost.cs b/src/Squidex/Config/Orleans/SiloHost.cs deleted file mode 100644 index 1817dfa12..000000000 --- a/src/Squidex/Config/Orleans/SiloHost.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Orleans.Hosting; -using Squidex.Config.Startup; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; - -namespace Squidex.Config.Orleans -{ - public sealed class SiloHost : SafeHostedService - { - private readonly ISiloHost silo; - - public SiloHost(ISiloHost silo, ISemanticLog log, IApplicationLifetime lifetime) - : base(lifetime, log) - { - this.silo = silo; - } - - protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) - { - var watch = ValueStopwatch.StartNew(); - try - { - await silo.StartAsync(ct); - } - finally - { - var elapsedMs = watch.Stop(); - - log.LogInformation(w => w - .WriteProperty("message", "Silo started") - .WriteProperty("elapsedMs", elapsedMs)); - } - } - - protected override async Task StopAsync(ISemanticLog log, CancellationToken ct) - { - await silo.StopAsync(); - } - } -} diff --git a/src/Squidex/Config/Startup/BackgroundHost.cs b/src/Squidex/Config/Startup/BackgroundHost.cs new file mode 100644 index 000000000..41bef8f72 --- /dev/null +++ b/src/Squidex/Config/Startup/BackgroundHost.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. + +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class BackgroundHost : SafeHostedService + { + private readonly IEnumerable targets; + + public BackgroundHost(IEnumerable targets, IApplicationLifetime lifetime, ISemanticLog log) + : base(lifetime, log) + { + this.targets = targets; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + foreach (var target in targets.Distinct()) + { + await target.StartAsync(ct); + + log.LogInformation(w => w.WriteProperty("backgroundSystem", target.ToString())); + } + } + } +} diff --git a/src/Squidex/Config/Web/WebExtensions.cs b/src/Squidex/Config/Web/WebExtensions.cs index b50350723..badab15c3 100644 --- a/src/Squidex/Config/Web/WebExtensions.cs +++ b/src/Squidex/Config/Web/WebExtensions.cs @@ -17,8 +17,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Net.Http.Headers; using Squidex.Infrastructure.Json; -using Squidex.Pipeline; using Squidex.Pipeline.Robots; +using Squidex.Web.Pipeline; namespace Squidex.Config.Web { diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs index 3de6a6f33..ec55d161a 100644 --- a/src/Squidex/Config/Web/WebServices.cs +++ b/src/Squidex/Config/Web/WebServices.cs @@ -6,16 +6,19 @@ // ========================================================================== using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Config.Domain; -using Squidex.Pipeline; +using Squidex.Pipeline.Plugins; using Squidex.Pipeline.Robots; +using Squidex.Web; +using Squidex.Web.Pipeline; namespace Squidex.Config.Web { public static class WebServices { - public static void AddMyMvc(this IServiceCollection services) + public static void AddMyMvcWithPlugins(this IServiceCollection services, IConfiguration config) { services.AddSingletonAs() .AsSelf(); @@ -46,7 +49,9 @@ namespace Squidex.Config.Web options.Filters.Add(); options.Filters.Add(); options.Filters.Add(); - }).AddMySerializers(); + }) + .AddMyPlugins(config) + .AddMySerializers(); services.AddCors(); services.AddRouting(); diff --git a/src/Squidex/Pipeline/Plugins/PluginExtensions.cs b/src/Squidex/Pipeline/Plugins/PluginExtensions.cs new file mode 100644 index 000000000..8f257bc51 --- /dev/null +++ b/src/Squidex/Pipeline/Plugins/PluginExtensions.cs @@ -0,0 +1,148 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using McMaster.NETCore.Plugins; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Plugins; +using Squidex.Web; + +namespace Squidex.Pipeline.Plugins +{ + public static class PluginExtensions + { + private static readonly Type[] SharedTypes = + { + typeof(IPlugin), + typeof(SquidexCoreModel), + typeof(SquidexCoreOperations), + typeof(SquidexEntities), + typeof(SquidexEvents), + typeof(SquidexInfrastructure), + typeof(SquidexWeb) + }; + + public static IMvcBuilder AddMyPlugins(this IMvcBuilder mvcBuilder, IConfiguration config) + { + var pluginManager = new PluginManager(); + + var options = config.Get(); + + if (options.Plugins != null) + { + foreach (var path in options.Plugins) + { + var plugin = LoadPlugin(path); + + if (plugin != null) + { + try + { + var pluginAssembly = plugin.LoadDefaultAssembly(); + + AddParts(mvcBuilder, pluginAssembly); + + foreach (var relatedAssembly in RelatedAssemblyAttribute.GetRelatedAssemblies(pluginAssembly, false)) + { + AddParts(mvcBuilder, relatedAssembly); + } + + pluginManager.Add(path, pluginAssembly); + } + catch (Exception ex) + { + pluginManager.LogException(path, "LoadingAssembly", ex); + } + } + else + { + pluginManager.LogException(path, "LoadingPlugin", new FileNotFoundException($"Cannot find plugin at {path}")); + } + } + } + + pluginManager.ConfigureServices(mvcBuilder.Services, config); + + mvcBuilder.Services.AddSingleton(pluginManager); + + return mvcBuilder; + } + + private static PluginLoader LoadPlugin(string pluginPath) + { + foreach (var candidate in GetPaths(pluginPath)) + { + if (candidate.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)) + { + return PluginLoader.CreateFromAssemblyFile(candidate.FullName, PluginLoaderOptions.PreferSharedTypes); + } + + if (candidate.Extension.Equals(".json", StringComparison.OrdinalIgnoreCase)) + { + return PluginLoader.CreateFromConfigFile(candidate.FullName, SharedTypes); + } + } + + return null; + } + + private static IEnumerable GetPaths(string pluginPath) + { + var candidate = new FileInfo(Path.GetFullPath(pluginPath)); + + if (candidate.Exists) + { + yield return candidate; + } + + if (!Path.IsPathRooted(pluginPath)) + { + candidate = new FileInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), pluginPath)); + + if (candidate.Exists) + { + yield return candidate; + } + } + } + + public static void UsePlugins(this IApplicationBuilder app) + { + var pluginManager = app.ApplicationServices.GetRequiredService(); + + pluginManager.Configure(app); + + var log = app.ApplicationServices.GetService(); + + if (log != null) + { + pluginManager.Log(log); + } + } + + private static void AddParts(IMvcBuilder mvcBuilder, Assembly assembly) + { + var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); + + foreach (var part in partFactory.GetApplicationParts(assembly)) + { + mvcBuilder.PartManager.ApplicationParts.Add(part); + } + } + } +} diff --git a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/src/Squidex/Pipeline/Squid/SquidMiddleware.cs index 0882194d2..a9f2e305a 100644 --- a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ b/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.IO; -using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -131,7 +130,7 @@ namespace Squidex.Pipeline.Squid private static string LoadSvg(string name) { - var assembly = typeof(SquidMiddleware).GetTypeInfo().Assembly; + var assembly = typeof(SquidMiddleware).Assembly; using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Pipeline.Squid.icon-{name}.svg")) { diff --git a/src/Squidex/Pipeline/Swagger/NSwagHelper.cs b/src/Squidex/Pipeline/Swagger/NSwagHelper.cs index 6cdc3be8d..385a2b48f 100644 --- a/src/Squidex/Pipeline/Swagger/NSwagHelper.cs +++ b/src/Squidex/Pipeline/Swagger/NSwagHelper.cs @@ -9,13 +9,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using NJsonSchema; using NJsonSchema.Generation; using NSwag; -using Squidex.Config; +using Squidex.Web; namespace Squidex.Pipeline.Swagger { @@ -23,7 +22,7 @@ namespace Squidex.Pipeline.Swagger { public static string LoadDocs(string name) { - var assembly = typeof(NSwagHelper).GetTypeInfo().Assembly; + var assembly = typeof(NSwagHelper).Assembly; using (var resourceStream = assembly.GetManifestResourceStream($"Squidex.Docs.{name}.md")) { @@ -34,7 +33,7 @@ namespace Squidex.Pipeline.Swagger } } - public static SwaggerDocument CreateApiDocument(HttpContext context, MyUrlsOptions urlOptions, string appName) + public static SwaggerDocument CreateApiDocument(HttpContext context, UrlsOptions urlOptions, string appName) { var scheme = string.Equals(context.Request.Scheme, "http", StringComparison.OrdinalIgnoreCase) ? diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 378fa8a7b..f7c33f443 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -38,16 +38,16 @@ + - + - - - + - + + @@ -59,23 +59,24 @@ + - - - - - - - - - - + + + + + + + + + + + - + - diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index ec885434c..29a9c580f 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -7,6 +7,7 @@ using System; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Migrate_01; @@ -14,6 +15,7 @@ using Squidex.Areas.Api; using Squidex.Areas.Api.Config.Swagger; using Squidex.Areas.Api.Controllers.Contents; using Squidex.Areas.Api.Controllers.News; +using Squidex.Areas.Api.Controllers.UI; using Squidex.Areas.Frontend; using Squidex.Areas.IdentityServer; using Squidex.Areas.IdentityServer.Config; @@ -27,32 +29,36 @@ using Squidex.Config.Startup; using Squidex.Config.Web; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Extensions.Actions.Twitter; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Diagnostics; -using Squidex.Pipeline; +using Squidex.Infrastructure.Translations; +using Squidex.Pipeline.Plugins; using Squidex.Pipeline.Robots; +using Squidex.Web; namespace Squidex { public sealed class WebStartup { - private readonly IConfiguration configuration; + private readonly IConfiguration config; + private readonly IHostingEnvironment environment; - public WebStartup(IConfiguration configuration) + public WebStartup(IConfiguration config, IHostingEnvironment environment) { - this.configuration = configuration; + this.config = config; + + this.environment = environment; } public IServiceProvider ConfigureServices(IServiceCollection services) { - var config = configuration; - services.AddHttpClient(); services.AddLogging(); services.AddMemoryCache(); services.AddOptions(); + services.AddMyMvcWithPlugins(config); + services.AddMyAssetServices(config); services.AddMyAuthentication(config); services.AddMyEntitiesServices(config); @@ -62,7 +68,6 @@ namespace Squidex services.AddMyInfrastructureServices(config); services.AddMyLoggingServices(config); services.AddMyMigrationServices(); - services.AddMyMvc(); services.AddMyRuleServices(); services.AddMySerializers(); services.AddMyStoreServices(config); @@ -73,40 +78,41 @@ namespace Squidex config.GetSection("contents")); services.Configure( config.GetSection("assets")); + services.Configure( + config.GetSection("translations:deepL")); services.Configure( config.GetSection("mode")); - services.Configure( - config.GetSection("twitter")); services.Configure( config.GetSection("robots")); services.Configure( config.GetSection("healthz:gc")); services.Configure( config.GetSection("etags")); + services.Configure( + config.GetSection("urls")); + services.Configure( + config.GetSection("usage")); services.Configure( config.GetSection("rebuild")); services.Configure( config.GetSection("contentsController")); - services.Configure( - config.GetSection("urls")); services.Configure( config.GetSection("identity")); services.Configure( config.GetSection("ui")); - services.Configure( - config.GetSection("usage")); services.Configure( config.GetSection("news")); - var provider = services.AddAndBuildOrleans(configuration, afterServices => - { - afterServices.AddHostedService(); - afterServices.AddHostedService(); - afterServices.AddHostedService(); - }); + services.AddHostedService(); + + services.AddOrleans(config, environment); - return provider; + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + + return services.BuildServiceProvider(); } public void Configure(IApplicationBuilder app) @@ -125,6 +131,8 @@ namespace Squidex app.ConfigureOrleansDashboard(); app.ConfigureIdentityServer(); app.ConfigureFrontend(); + + app.UsePlugins(); } } } diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.html b/src/Squidex/app/features/administration/pages/restore/restore-page.component.html index d11ddcd7d..096a355db 100644 --- a/src/Squidex/app/features/administration/pages/restore/restore-page.component.html +++ b/src/Squidex/app/features/administration/pages/restore/restore-page.component.html @@ -6,48 +6,46 @@ - -
-
-
-
-
- -
-
- -
-
- -
+
+
+
+
+
+
- -
-

Last Restore Operation

+
+
- -
- {{job.url}} +
+
-
-
-
- {{row}} + +
+

Last Restore Operation

+
+ +
+ {{job.url}}
-
diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.html b/src/Squidex/app/features/administration/pages/users/user-page.component.html index 0c19b2998..3f8918635 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.html @@ -16,13 +16,11 @@ - - - - - + + + diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html index dde3ab83c..a65cdab0e 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -47,8 +47,8 @@
- - +
+ diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index 6bfb9fa97..3a9d658e9 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -100,9 +100,13 @@ - + + - + + \ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.html b/src/Squidex/app/features/assets/pages/assets-filters-page.component.html index 0a32d5244..f6c48e822 100644 --- a/src/Squidex/app/features/assets/pages/assets-filters-page.component.html +++ b/src/Squidex/app/features/assets/pages/assets-filters-page.component.html @@ -31,7 +31,10 @@

Saved queries

+ [class.active]="isSelectedQuery(query.filter)"> + + {{query.name}} + diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index fd527a854..d27447adc 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -30,6 +30,7 @@ + - + + \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content-status.component.html b/src/Squidex/app/features/content/shared/content-status.component.html index 22de0d0b8..b03f43a5b 100644 --- a/src/Squidex/app/features/content/shared/content-status.component.html +++ b/src/Squidex/app/features/content/shared/content-status.component.html @@ -1,13 +1,13 @@ - - - + + + - - - + + + - +{{displayStatus}} \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content-status.component.ts b/src/Squidex/app/features/content/shared/content-status.component.ts index 527002671..85011a079 100644 --- a/src/Squidex/app/features/content/shared/content-status.component.ts +++ b/src/Squidex/app/features/content/shared/content-status.component.ts @@ -34,10 +34,16 @@ export class ContentStatusComponent { @Input() public alignMiddle = true; - public get displayStatus() { + public get tooltipText() { if (this.scheduledAt) { return `Will be set to '${this.scheduledTo}' at ${this.scheduledAt.toStringFormat('LLLL')}`; - } else if (this.isPending) { + } else { + return this.status; + } + } + + public get displayStatus() { + if (this.isPending) { return 'Pending'; } else { return this.status; diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.html b/src/Squidex/app/features/content/shared/contents-selector.component.html index a174dd63f..6b57c596f 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.html +++ b/src/Squidex/app/features/content/shared/contents-selector.component.html @@ -48,8 +48,8 @@
-
@@ -60,7 +60,7 @@ {{userInfo.user.email}} - + @@ -68,10 +68,12 @@ - - + + + +
- +
+ - - - {{dueTimeAction}} content item(s) - + + + {{dueTimeAction}} content item(s) + - -
- - -
+ +
+ + +
-
- - -
+
+ + +
- -
+ +
- - - - -
- + + + + +
diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts index 6b65eccfb..e8d6e49d0 100644 --- a/src/Squidex/app/features/rules/declarations.ts +++ b/src/Squidex/app/features/rules/declarations.ts @@ -5,17 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -export * from './pages/rules/actions/algolia-action.component'; -export * from './pages/rules/actions/azure-queue-action.component'; -export * from './pages/rules/actions/discourse-action.component'; -export * from './pages/rules/actions/elastic-search-action.component'; -export * from './pages/rules/actions/email-action.component'; -export * from './pages/rules/actions/fastly-action.component'; -export * from './pages/rules/actions/medium-action.component'; -export * from './pages/rules/actions/prerender-action.component'; -export * from './pages/rules/actions/slack-action.component'; -export * from './pages/rules/actions/tweet-action.component'; -export * from './pages/rules/actions/webhook-action.component'; +export * from './pages/rules/actions/generic-action.component'; export * from './pages/rules/triggers/asset-changed-trigger.component'; export * from './pages/rules/triggers/content-changed-trigger.component'; diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index 95551399c..7ee3218b3 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -15,26 +15,16 @@ import { } from '@app/shared'; import { - AlgoliaActionComponent, AssetChangedTriggerComponent, - AzureQueueActionComponent, ContentChangedTriggerComponent, - DiscourseActionComponent, - ElasticSearchActionComponent, - EmailActionComponent, - FastlyActionComponent, - MediumActionComponent, - PrerenderActionComponent, + GenericActionComponent, RuleElementComponent, RuleEventBadgeClassPipe, RuleEventsPageComponent, RulesPageComponent, RuleWizardComponent, SchemaChangedTriggerComponent, - SlackActionComponent, - TweetActionComponent, - UsageTriggerComponent, - WebhookActionComponent + UsageTriggerComponent } from './declarations'; const routes: Routes = [ @@ -64,26 +54,16 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ - AlgoliaActionComponent, AssetChangedTriggerComponent, - AzureQueueActionComponent, ContentChangedTriggerComponent, - DiscourseActionComponent, - EmailActionComponent, - ElasticSearchActionComponent, - FastlyActionComponent, - MediumActionComponent, - PrerenderActionComponent, + GenericActionComponent, RuleElementComponent, RuleEventBadgeClassPipe, RuleEventsPageComponent, RulesPageComponent, RuleWizardComponent, SchemaChangedTriggerComponent, - SlackActionComponent, - TweetActionComponent, - UsageTriggerComponent, - WebhookActionComponent + UsageTriggerComponent ] }) export class SqxFeatureRulesModule { } \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html deleted file mode 100644 index 10e8f129e..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html +++ /dev/null @@ -1,43 +0,0 @@ -
-
- - -
- - - - - - The ID to you algolia application. - -
-
- -
- - -
- - - - - - The API Key to access you algolia app. - -
-
- -
- - -
- - - - - - The name of the index. You can use advanced formatting (read help section). - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.scss deleted file mode 100644 index fbb752506..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_vars'; -@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts deleted file mode 100644 index 0ac2c1641..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-algolia-action', - styleUrls: ['./algolia-action.component.scss'], - templateUrl: './algolia-action.component.html' -}) -export class AlgoliaActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('appId', - new FormControl(this.action.appId || '', [ - Validators.required - ])); - - this.actionForm.setControl('apiKey', - new FormControl(this.action.apiKey || '', [ - Validators.required - ])); - - this.actionForm.setControl('indexName', - new FormControl(this.action.indexName || '$SCHEMA_NAME', [ - Validators.required - ])); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html deleted file mode 100644 index ffae8dd67..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html +++ /dev/null @@ -1,29 +0,0 @@ - -
- - -
- - - - - - The connection string to the storage account. - -
-
- -
- - -
- - - - - - The name of the queue. - -
-
- \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.scss deleted file mode 100644 index fbb752506..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_vars'; -@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts deleted file mode 100644 index 8df30f90e..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -import { ValidatorsEx } from '@app/shared'; - -@Component({ - selector: 'sqx-azure-queue-action', - styleUrls: ['./azure-queue-action.component.scss'], - templateUrl: './azure-queue-action.component.html' -}) -export class AzureQueueActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('connectionString', - new FormControl(this.action.connectionString || '', [ - Validators.required - ])); - - this.actionForm.setControl('queue', - new FormControl(this.action.queue || 'squidex', [ - Validators.required, - ValidatorsEx.pattern('[a-z][a-z0-9]{2,}(\-[a-z0-9]+)*', 'Name must be a valid azure queue name.') - ])); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.html deleted file mode 100644 index e22fd9080..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.html +++ /dev/null @@ -1,99 +0,0 @@ -
-
- - -
- - - - - - The url to your discourse server. - -
-
- -
- - -
- - - - - - The api key to authenticate to your discourse server. - -
-
- -
- - -
- - - - - - The api username to authenticate to your discourse server. - -
-
- -
- - -
- - - - - - The text for your topic or post. Read the help section for information about advanced formatting. - -
-
- -
- - -
- - - - - - The optional title, when you want to create a topic. Read the help section for information about advanced formatting. - -
-
- -
- - -
- - - - - - The topic id when you want to create a post. - -
-
- -
- - -
- - - - - - The category id when you create a topic. - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.scss deleted file mode 100644 index 756609665..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -textarea { - height: 150px; -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.ts deleted file mode 100644 index a2e25ac55..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/discourse-action.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-discourse-action', - styleUrls: ['./discourse-action.component.scss'], - templateUrl: './discourse-action.component.html' -}) -export class DiscourseActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('url', - new FormControl(this.action.url || '', [ - Validators.required - ])); - - this.actionForm.setControl('apiKey', - new FormControl(this.action.apiKey || '', [ - Validators.required - ])); - - this.actionForm.setControl('apiUsername', - new FormControl(this.action.apiUsername || '', [ - Validators.required - ])); - - this.actionForm.setControl('text', - new FormControl(this.action.text || '', [ - Validators.required - ])); - - this.actionForm.setControl('title', - new FormControl(this.action.title)); - - this.actionForm.setControl('topic', - new FormControl(this.action.topic)); - - this.actionForm.setControl('category', - new FormControl(this.action.category)); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html deleted file mode 100644 index fee16364c..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html +++ /dev/null @@ -1,71 +0,0 @@ -
-
- - -
- - - - - - The url to your elastic search instance. - -
-
- -
- - -
- - - - - - The username for authentication. Highly recommended. - -
-
- -
- - -
- - - - - - The password for authentication. Highly recommended. - -
-
- -
- - -
- - - - - - The name of the index. You can use advanced formatting (read help section). - -
-
- -
- - -
- - - - - - The name of the type. You can use advanced formatting (read help section). - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.scss deleted file mode 100644 index fbb752506..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_vars'; -@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts deleted file mode 100644 index 8e6e0c8f2..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-elastic-search-action', - styleUrls: ['./elastic-search-action.component.scss'], - templateUrl: './elastic-search-action.component.html' -}) -export class ElasticSearchActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('host', - new FormControl(this.action.host || '', [ - Validators.required - ])); - - this.actionForm.setControl('indexName', - new FormControl(this.action.indexName || '$APP_NAME', [ - Validators.required - ])); - - this.actionForm.setControl('indexType', - new FormControl(this.action.indexType || '$SCHEMA_NAME')); - - this.actionForm.setControl('username', - new FormControl(this.action.username)); - - this.actionForm.setControl('password', - new FormControl(this.action.password)); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.html deleted file mode 100644 index c083bb1cc..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.html +++ /dev/null @@ -1,128 +0,0 @@ -
-
- - -
- - - - - - The IP address or host to the SMTP server. - -
-
- -
- - -
- - - - - - The port to the SMTP server. - -
-
- -
-
-
- - -
- - Specify whether the SMTP client uses Secure Sockets Layer (SSL) to encrypt the connection. - -
-
- -
- - -
- - - - - - The username for the SMTP server. - -
-
- -
- - -
- - - - - - The password for the SMTP server. - -
-
- -
- - -
- - - - - - The email sending address. Read the help section for information about advanced formatting. - -
-
- -
- - -
- - - - - - The email message will be sent to. Read the help section for information about advanced formatting. - -
-
- -
- - -
- - - - - - The subject line for this email message. Read the help section for information about advanced formatting. - -
-
- - -
- - -
- - - - - - The message body. Read the help section for information about advanced formatting. - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.scss deleted file mode 100644 index 51468cba5..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -textarea { - height: 250px; -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.ts deleted file mode 100644 index 31761bef8..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/email-action.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-email-action', - styleUrls: ['./email-action.component.scss'], - templateUrl: './email-action.component.html' -}) -export class EmailActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('serverHost', - new FormControl(this.action.serverHost || 'smtp.gmail.com', [ - Validators.required - ])); - - this.actionForm.setControl('serverPort', - new FormControl(this.action.serverPort || 465, [ - Validators.required - ])); - - this.actionForm.setControl('serverUseSsl', - new FormControl(this.action.serverUseSsl || true)); - - this.actionForm.setControl('serverUsername', - new FormControl(this.action.serverUsername || '', [ - Validators.required - ])); - - this.actionForm.setControl('serverPassword', - new FormControl(this.action.serverPassword || '', [ - Validators.required - ])); - - this.actionForm.setControl('messageFrom', - new FormControl(this.action.messageFrom || '', [ - Validators.required - ])); - - this.actionForm.setControl('messageTo', - new FormControl(this.action.messageTo || '', [ - Validators.required - ])); - - this.actionForm.setControl('messageSubject', - new FormControl(this.action.messageSubject || '', [ - Validators.required - ])); - - this.actionForm.setControl('messageBody', - new FormControl(this.action.messageBody || '', [ - Validators.required - ])); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html deleted file mode 100644 index b089b622c..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
- - -
- - - - - - The service ID of the fastly account. - -
-
- -
- - -
- - - - - - The API key for the fastly account. - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.scss deleted file mode 100644 index fbb752506..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_vars'; -@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts deleted file mode 100644 index 192216769..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-fastly-action', - styleUrls: ['./fastly-action.component.scss'], - templateUrl: './fastly-action.component.html' -}) -export class FastlyActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('serviceId', - new FormControl(this.action.serviceId || '', [ - Validators.required - ])); - - this.actionForm.setControl('apiKey', - new FormControl(this.action.apiKey || '', [ - Validators.required - ])); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.html new file mode 100644 index 000000000..3ab92a516 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.html @@ -0,0 +1,36 @@ +
+
+ + +
+ + +
+
+ +
+
+
+ + +
+
+
+ +
+
+ + + {{property.description}} + + + You can use advanced formatting: Documentation + + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.scss new file mode 100644 index 000000000..91941c2fc --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.scss @@ -0,0 +1,10 @@ +@import '_vars'; +@import '_mixins'; + +.form-check { + margin-bottom: -.25rem; +} + +textarea { + height: 15rem; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts new file mode 100644 index 000000000..06992b17e --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/actions/generic-action.component.ts @@ -0,0 +1,44 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +import { RuleElementDto } from '@app/shared'; + +@Component({ + selector: 'sqx-generic-action', + styleUrls: ['./generic-action.component.scss'], + templateUrl: './generic-action.component.html' +}) +export class GenericActionComponent implements OnInit { + @Input() + public definition: RuleElementDto; + + @Input() + public action: any; + + @Input() + public actionForm: FormGroup; + + @Input() + public actionFormSubmitted = false; + + public ngOnInit() { + for (let property of this.definition.properties) { + const validators = []; + + if (property.isRequired) { + validators.push(Validators.required); + } + + const control = new FormControl(this.action[property.name] || '', validators); + + this.actionForm.setControl(property.name, control); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.html deleted file mode 100644 index d92a5c7e8..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.html +++ /dev/null @@ -1,96 +0,0 @@ -
-
- - -
- - - - - - The self issued access token. Can be created under https://medium.com/me/settings. - -
-
- -
- - -
- - - - - - The title of the post. Note that this title is used for SEO and when rendering the post as a listing. - -
-
- -
- - -
- - - - - - The body of the post, in a valid, semantic, HTML fragment, or Markdown. - -
-
- -
-
-
- - -
-
-
- -
- - -
- - - - - - The original home of this content, if it was originally published elsewhere. - -
-
- -
- - -
- - - - - - Optional comma-separated list of tags. - -
-
- -
- - -
- - - - - - Optional publication id. Go to https://medium.com/[PUBLICATION]?format=json to fetch the id. - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.scss deleted file mode 100644 index 756609665..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -textarea { - height: 150px; -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts deleted file mode 100644 index a14f0841c..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/medium-action.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-medium-action', - styleUrls: ['./medium-action.component.scss'], - templateUrl: './medium-action.component.html' -}) -export class MediumActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('accessToken', - new FormControl(this.action.accessToken || '', [ - Validators.required - ])); - - this.actionForm.setControl('title', - new FormControl(this.action.title || '', [ - Validators.required - ])); - - this.actionForm.setControl('content', - new FormControl(this.action.content || '', [ - Validators.required - ])); - - this.actionForm.setControl('canonicalUrl', - new FormControl(this.action.canonicalUrl || '')); - - this.actionForm.setControl('tags', - new FormControl(this.action.tags || '')); - - this.actionForm.setControl('publicationId', - new FormControl(this.action.publicationId || '')); - - this.actionForm.setControl('isHtml', - new FormControl(this.action.isHtml || false)); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.html deleted file mode 100644 index 63b5a2319..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
- - -
- - - - - - The prerender token from your account. - -
-
- -
- - -
- - - - - - The url to recache. Read the help section for information about advanced formatting. - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.scss deleted file mode 100644 index fbb752506..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_vars'; -@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.ts deleted file mode 100644 index adf25fb6d..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/prerender-action.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-prerender-action', - styleUrls: ['./prerender-action.component.scss'], - templateUrl: './prerender-action.component.html' -}) -export class PrerenderActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('token', - new FormControl(this.action.token || '', [ - Validators.required - ])); - - this.actionForm.setControl('url', - new FormControl(this.action.url || '', [ - Validators.required - ])); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html deleted file mode 100644 index 0f21696a0..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html +++ /dev/null @@ -1,29 +0,0 @@ - -
- - -
- - - - - - The url to the incoming slack webhook. - -
-
- -
- - -
- - - - - - The text to send to slack. Read the help section for information about advanced formatting. - -
-
- \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.scss deleted file mode 100644 index 756609665..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -textarea { - height: 150px; -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts deleted file mode 100644 index 9a425591b..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-slack-action', - styleUrls: ['./slack-action.component.scss'], - templateUrl: './slack-action.component.html' -}) -export class SlackActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('webhookUrl', - new FormControl(this.action.webhookUrl || '', [ - Validators.required - ])); - - this.actionForm.setControl('text', - new FormControl(this.action.text || '', [ - Validators.required - ])); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html deleted file mode 100644 index 341d20043..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html +++ /dev/null @@ -1,55 +0,0 @@ - -
-
- - - - - - - - - - - -
-
- -
- - -
- - - -
-
- -
- - -
- - - -
-
- -
- - -
- - - - - - The text to tweet. Read the help section for information about advanced formatting. - -
-
- \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss deleted file mode 100644 index 756609665..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '_vars'; -@import '_mixins'; - -textarea { - height: 150px; -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts deleted file mode 100644 index 84b8b802c..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { HttpClient } from '@angular/common/http'; -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -import { DialogService } from '@app/shared'; - -@Component({ - selector: 'sqx-tweet-action', - styleUrls: ['./tweet-action.component.scss'], - templateUrl: './tweet-action.component.html' -}) -export class TweetActionComponent implements OnInit { - private request: any; - - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public isAuthenticating = false; - public isRedirected = false; - - public pinCode: string; - - constructor( - private readonly dialogs: DialogService, - private readonly httpClient: HttpClient - ) { - } - - public ngOnInit() { - this.actionForm.setControl('accessToken', - new FormControl(this.action.accessToken || '', [ - Validators.required - ])); - - this.actionForm.setControl('accessSecret', - new FormControl(this.action.accessSecret || '', [ - Validators.required - ])); - - this.actionForm.setControl('text', - new FormControl(this.action.text || '', [ - Validators.required, - Validators.maxLength(280) - ])); - } - - public auth() { - this.isAuthenticating = true; - - this.httpClient.get('api/rules/twitter/auth') - .subscribe((response: any) => { - this.request = { - requestToken: response.requestToken, - requestTokenSecret: response.requestTokenSecret - }; - - this.isAuthenticating = false; - this.isRedirected = true; - - window.open(response.authorizeUri, '_blank'); - }, () => { - this.dialogs.notifyError('Failed to authenticate with twitter.'); - - this.isAuthenticating = false; - this.isRedirected = false; - }); - } - - public complete() { - this.request.pinCode = this.pinCode; - - this.httpClient.post('api/rules/twitter/token', this.request) - .subscribe((response: any) => { - this.actionForm.get('accessToken')!.setValue(response.accessToken); - this.actionForm.get('accessSecret')!.setValue(response.accessTokenSecret); - - this.isRedirected = false; - }, () => { - this.dialogs.notifyError('Failed to request access token.'); - - this.isAuthenticating = false; - this.isRedirected = false; - }); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html deleted file mode 100644 index b8d4bb1d8..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
- - -
- - - - - - The url where the events will be sent to. - -
-
- -
- - -
- - - - - - The shared secret will be used to add a header X-Signature=Base64(Sha256(RequestBody + Secret)) - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss deleted file mode 100644 index fbb752506..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_vars'; -@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts b/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts deleted file mode 100644 index 767ed1442..000000000 --- a/src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; - -@Component({ - selector: 'sqx-webhook-action', - styleUrls: ['./webhook-action.component.scss'], - templateUrl: './webhook-action.component.html' -}) -export class WebhookActionComponent implements OnInit { - @Input() - public action: any; - - @Input() - public actionForm: FormGroup; - - @Input() - public actionFormSubmitted = false; - - public ngOnInit() { - this.actionForm.setControl('url', - new FormControl(this.action.url || '', [ - Validators.required - ])); - - this.actionForm.setControl('sharedSecret', - new FormControl(this.action.sharedSecret || '')); - } -} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/rule-element.component.html b/src/Squidex/app/features/rules/pages/rules/rule-element.component.html index 6ad970abc..efcc0fb23 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-element.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rule-element.component.html @@ -1,20 +1,27 @@ -
-
- +
+
+ + + + +
-
-
- {{element.display}} -
+
+ {{element.display}}
+
-
+ + + + +
diff --git a/src/Squidex/app/features/rules/pages/rules/rule-element.component.scss b/src/Squidex/app/features/rules/pages/rules/rule-element.component.scss index fca4de50b..aab579f44 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-element.component.scss +++ b/src/Squidex/app/features/rules/pages/rules/rule-element.component.scss @@ -5,9 +5,12 @@ & { @include transition(background-color .4s ease); cursor: pointer; + height: 3rem; + position: relative; } &-text { + @include absolute(0, 0, 0, 3rem); @include truncate; color: $color-dark-foreground; line-height: 3rem; @@ -17,14 +20,16 @@ } &-icon { + @include absolute(0, auto, 0, 0); + color: $color-dark-foreground; line-height: 3.2rem; font-size: 1.2rem; font-weight: normal; padding: 0 .8rem; } - .col { - height: 3rem; + .icon { + font-size: 20px; } .svg-icon { @@ -56,6 +61,7 @@ } &-icon { + color: $color-dark-foreground; display: inline-block; margin-right: .5rem; position: relative; @@ -63,6 +69,10 @@ line-height: 1px; } + .icon { + font-size: 30px; + } + .svg-icon { width: 30px; } 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 16f1fca8e..9c5b40f42 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 @@ -79,85 +79,12 @@

{{ruleActions[actionType].display}}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html index 313951c46..ba7e7a540 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -19,58 +19,59 @@ - - -
- No Rule created yet. + +
+ No rule created yet. - -
+ +
-
- - - - - - - - - - - -
-

If

-
- - - - -

then

-
- - - - - - - -
+ + + + + + + + + + + + +
+

If

+
+ + + + +

then

+
+ + + + + + + +
- - - - + + + diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts index 849f26c94..3c21249db 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.ts @@ -9,6 +9,7 @@ import { Component, OnInit } from '@angular/core'; import { onErrorResumeNext } from 'rxjs/operators'; import { + ALL_TRIGGERS, AppsState, DialogModel, RuleDto, @@ -30,7 +31,7 @@ export class RulesPageComponent implements OnInit { public wizardRule: RuleDto | null; public ruleActions: { [name: string]: RuleElementDto }; - public ruleTriggers: { [name: string]: RuleElementDto }; + public ruleTriggers = ALL_TRIGGERS; constructor( public readonly appsState: AppsState, @@ -48,11 +49,6 @@ export class RulesPageComponent implements OnInit { this.ruleActions = actions; }); - this.rulesService.getTriggers() - .subscribe(triggers => { - this.ruleTriggers = triggers; - }); - this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); } diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html index 509579d6e..13c866d67 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html @@ -4,6 +4,7 @@ Clone Schema + Create Schema diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html index 8a04729f8..2254f8730 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html @@ -18,77 +18,75 @@ - +
Your have reached the maximum number of backups: 10.
- -
- No backups created yet. +
+ No backups created yet. - -
- -
-
-
-
- -
-
- -
-
- -
+ +
+ +
+
+
+
+
-
-
- Started: -
-
- Duration: -
+
+
-
-
- {{backup.started | sqxFromNow}} -
-
- {{backup | sqxBackupDuration}} -
+
+
-
-
- - Events: {{backup.handledEvents | sqxKNumber}} - , - - Assets: {{backup.handledAssets | sqxKNumber}} - -
-
- Download: - - - Ready - -
+
+
+
+ Started: +
+
+ Duration: +
+
+
+
+ {{backup.started | sqxFromNow}}
-
- +
+ {{backup | sqxBackupDuration}}
+
+
+ + Events: {{backup.handledEvents | sqxKNumber}} + , + + Assets: {{backup.handledAssets | sqxKNumber}} + +
+
+ Download: + + + Ready + +
+
+
+ +
- +
diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html index 654781be9..476038fd9 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.html +++ b/src/Squidex/app/features/settings/pages/clients/client.component.html @@ -2,7 +2,7 @@
-
+
@@ -16,13 +16,13 @@ - +

{{client.name}}

-
+
diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html index b0fff5866..5ef4d921b 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html @@ -14,7 +14,7 @@ - +
No client created yet.
diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html index ae44142e4..989801c71 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html @@ -14,14 +14,12 @@
- - - - - + + +