Browse Source

Merge pull request #349 from Squidex/feature/plugins

Feature/plugins
pull/351/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
90ae85e76a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      Dockerfile
  2. 4
      Dockerfile.build
  3. 46
      Squidex.sln
  4. 10
      extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
  5. 21
      extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs
  6. 8
      extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
  7. 21
      extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs
  8. 15
      extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
  9. 3
      extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
  10. 21
      extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs
  11. 11
      extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
  12. 4
      extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  13. 21
      extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs
  14. 38
      extensions/Squidex.Extensions/Actions/Email/EmailAction.cs
  15. 21
      extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs
  16. 4
      extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
  17. 21
      extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs
  18. 19
      extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
  19. 21
      extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs
  20. 5
      extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
  21. 21
      extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs
  22. 31
      extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs
  23. 85
      extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs
  24. 5
      extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
  25. 21
      extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs
  26. 57
      extensions/Squidex.Extensions/Actions/TriggerTypes.cs
  27. 6
      extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
  28. 24
      extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs
  29. 9
      extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
  30. 21
      extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs
  31. 29
      extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs
  32. 6
      extensions/Squidex.Extensions/Squidex.Extensions.csproj
  33. 6
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
  34. 4
      src/Squidex.Domain.Apps.Core.Model/SquidexCoreModel.cs
  35. 25
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/DependencyInjectionExtensions.cs
  36. 16
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/FormattableAttribute.cs
  37. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionAttribute.cs
  38. 7
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionDefinition.cs
  39. 24
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionProperty.cs
  40. 20
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionPropertyEditor.cs
  41. 24
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionRegistration.cs
  42. 140
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleRegistry.cs
  43. 2
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  44. 18
      src/Squidex.Domain.Apps.Core.Operations/SquidexCoreOperations.cs
  45. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  46. 7
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  47. 167
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  48. 152
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs
  49. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  50. 71
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs
  51. 104
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  52. 8
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  53. 42
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  54. 59
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
  55. 12
      src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
  56. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  57. 5
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  58. 16
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  59. 2
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  60. 5
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  61. 4
      src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  62. 2
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  63. 2
      src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
  64. 5
      src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs
  65. 6
      src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs
  66. 2
      src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs
  67. 5
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  68. 18
      src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs
  69. 2
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  70. 2
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  71. 105
      src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs
  72. 24
      src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexer.cs
  73. 24
      src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndexerGrain.cs
  74. 18
      src/Squidex.Domain.Apps.Entities/Contents/Text/IndexData.cs
  75. 65
      src/Squidex.Domain.Apps.Entities/Contents/Text/MultiLanguageAnalyzer.cs
  76. 94
      src/Squidex.Domain.Apps.Entities/Contents/Text/PersistenceHelper.cs
  77. 18
      src/Squidex.Domain.Apps.Entities/Contents/Text/SearchContext.cs
  78. 297
      src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexerGrain.cs
  79. 5
      src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
  80. 5
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  81. 2
      src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
  82. 5
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  83. 2
      src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
  84. 8
      src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  85. 2
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs
  86. 2
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs
  87. 2
      src/Squidex.Domain.Apps.Entities/SquidexEntities.cs
  88. 4
      src/Squidex.Domain.Apps.Events/SquidexEvents.cs
  89. 4
      src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  90. 10
      src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs
  91. 32
      src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs
  92. 16
      src/Squidex.Infrastructure.Azure/EventSourcing/Constants.cs
  93. 33
      src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEvent.cs
  94. 33
      src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventCommit.cs
  95. 124
      src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs
  96. 142
      src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs
  97. 149
      src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs
  98. 150
      src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs
  99. 156
      src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs
  100. 62
      src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs

4
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

4
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

46
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}

10
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16 .842C7.633.842.842 7.625.842 16S7.625 31.158 16 31.158c8.374 0 15.158-6.791 15.158-15.166S24.375.842 16 .842zm0 25.83c-5.898 0-10.68-4.781-10.68-10.68S10.101 5.313 16 5.313s10.68 4.781 10.68 10.679-4.781 10.68-10.68 10.68zm0-19.156v7.956c0 .233.249.388.458.279l7.055-3.663a.312.312 0 0 0 .124-.434 8.807 8.807 0 0 0-7.319-4.447z'/></svg>",
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; }
}
}

21
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<AlgoliaAction, AlgoliaActionHandler>();
}
}
}

8
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M.011 16L0 6.248l12-1.63V16zM14 4.328L29.996 2v14H14zM30 18l-.004 14L14 29.75V18zM12 29.495L.01 27.851.009 18H12z'/></svg>",
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<ValidationError> CustomValidate()

21
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<AzureQueueAction, AzureQueueActionHandler>();
}
}
}

15
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16.137 0C7.376 0 0 7.037 0 15.721V32l16.134-.016C24.895 31.984 32 24.676 32 15.995S24.888 0 16.137 0zm.336 6.062a9.862 9.862 0 0 1 5.119 1.555l-.038-.023a.747.747 0 0 1 .05.033l-.033-.021c.288.183.529.353.762.534l-.022-.016c.058.044.094.073.131.103l-.018-.014c.218.174.411.34.597.514l-.005-.005a9.48 9.48 0 0 1 .639.655l.009.01c.073.082.154.176.233.272l.014.018c.053.06.116.133.177.206l.013.017-.052-.047-.008-.007c.104.126.218.273.328.423l.02.028.001.001-.001-.001c-.01-.018.005.005.019.028l.024.042c.145.206.301.451.445.704l.025.048c.131.226.273.51.402.801l.025.063a9.504 9.504 0 0 1 .802 3.853c0 5.38-4.401 9.741-9.831 9.741a9.866 9.866 0 0 1-4.106-.888l.061.025-6.39 1.43 1.78-5.672a7.888 7.888 0 0 1-.293-.584l-.025-.061a8.226 8.226 0 0 1-.254-.617l-.022-.068A1.043 1.043 0 0 1 7 19.017l-.022-.067a8.428 8.428 0 0 1-.246-.829l-.014-.067a9.402 9.402 0 0 1-.265-2.248c0-5.381 4.403-9.744 9.834-9.744l.194.002h-.01z'/></svg>",
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; }
}
}

3
extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs

@ -34,7 +34,6 @@ namespace Squidex.Extensions.Actions.Discourse
var json = new Dictionary<string, object>
{
["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 :

21
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<DiscourseAction, DiscourseActionHandler>();
}
}
}

11
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 28'><path d='M13.427 17.436H4.163C3.827 16.354 3.636 15.2 3.636 14s.182-2.355.527-3.436h15.245c1.891 0 3.418 1.545 3.418 3.445a3.421 3.421 0 0 1-3.418 3.427h-5.982zm-.436 1.146H4.6a11.508 11.508 0 0 0 4.2 4.982 11.443 11.443 0 0 0 15.827-3.209 5.793 5.793 0 0 0-4.173-1.773H12.99zm7.464-9.164a5.794 5.794 0 0 0 4.173-1.773 11.45 11.45 0 0 0-9.536-5.1c-2.327 0-4.491.7-6.3 1.891a11.554 11.554 0 0 0-4.2 4.982h15.864z'/></svg>",
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; }
}
}

4
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 ||

21
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<ElasticSearchAction, ElasticSearchActionHandler>();
}
}
}

38
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M28 5h-24c-2.209 0-4 1.792-4 4v13c0 2.209 1.791 4 4 4h24c2.209 0 4-1.791 4-4v-13c0-2.208-1.791-4-4-4zM2 10.25l6.999 5.25-6.999 5.25v-10.5zM30 22c0 1.104-0.898 2-2 2h-24c-1.103 0-2-0.896-2-2l7.832-5.875 4.368 3.277c0.533 0.398 1.166 0.6 1.8 0.6 0.633 0 1.266-0.201 1.799-0.6l4.369-3.277 7.832 5.875zM30 20.75l-7-5.25 7-5.25v10.5zM17.199 18.602c-0.349 0.262-0.763 0.4-1.199 0.4s-0.851-0.139-1.2-0.4l-12.8-9.602c0-1.103 0.897-2 2-2h24c1.102 0 2 0.897 2 2l-12.801 9.602z'/></svg>",
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; }
}
}

21
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<EmailAction, EmailActionHandler>();
}
}
}

4
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 32'><path d='M10.68.948v1.736h.806v2.6A12.992 12.992 0 0 0 .951 18.051c0 7.178 5.775 12.996 12.9 12.996 7.124 0 12.9-5.819 12.9-12.996-.004-6.332-4.502-11.605-10.455-12.755l-.081-.013V2.684h.807V.948H10.68zm3.53 10.605c3.218.173 5.81 2.713 6.09 5.922v.211h-.734v.737h.734v.201c-.279 3.21-2.871 5.752-6.09 5.925v-.723h-.733v.721c-3.281-.192-5.905-2.845-6.077-6.152h.728v-.737h-.724c.195-3.284 2.808-5.911 6.073-6.103v.725h.733v-.727zm2.513 3.051l-2.462 2.282a1.13 1.13 0 0 0-.41-.078c-.633 0-1.147.517-1.147 1.155a1.15 1.15 0 0 0 1.147 1.155c.633 0 1.147-.517 1.147-1.155 0-.117-.018-.23-.05-.337l.002.008 2.223-2.505-.449-.526z'/></svg>",
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; }
}
}

21
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<FastlyAction, FastlyActionHandler>();
}
}
}

19
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M3.795 8.48a1.239 1.239 0 0 0-.404-1.045l-2.987-3.6v-.537H9.68l7.171 15.727 6.304-15.727H32v.537l-2.556 2.449a.749.749 0 0 0-.284.717v18a.749.749 0 0 0 .284.716l2.493 2.449v.537H19.39v-.537l2.583-2.509c.253-.253.253-.328.253-.717V10.392l-7.187 18.251h-.969L5.703 10.392v12.232a1.69 1.69 0 0 0 .463 1.404l3.36 4.08v.536H-.001v-.537l3.36-4.08c.36-.371.52-.893.435-1.403V8.48z'/></svg>",
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; }
}
}

21
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<MediumAction, MediumActionHandler>();
}
}
}

5
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M2.073 17.984l8.646-5.36v-1.787L.356 17.325v1.318l10.363 6.488v-1.787zM29.927 17.984l-8.646-5.36v-1.787l10.363 6.488v1.318l-10.363 6.488v-1.787zM18.228 6.693l-6.276 19.426 1.656.548 6.276-19.426z'/></svg>",
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; }
}
}

21
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<PrerenderAction, PrerenderActionHandler>();
}
}
}

31
extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs

@ -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));
}
}
}
}

85
extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs

@ -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<Type> ActionHandlerTypes = new HashSet<Type>();
private static readonly Dictionary<string, RuleElement> ActionTypes = new Dictionary<string, RuleElement>();
public static IReadOnlyDictionary<string, RuleElement> Triggers
{
get { return TriggerTypes.All; }
}
public static IReadOnlyDictionary<string, RuleElement> Actions
{
get { return ActionTypes; }
}
public static IReadOnlyCollection<Type> ActionHandlers
{
get { return ActionHandlerTypes; }
}
static RuleElementRegistry()
{
var actionTypes =
typeof(RuleElementRegistry).Assembly
.GetTypes()
.Where(x => typeof(RuleAction).IsAssignableFrom(x))
.Where(x => x.GetCustomAttribute<RuleActionAttribute>() != null)
.Where(x => x.GetCustomAttribute<RuleActionHandlerAttribute>() != null)
.ToList();
foreach (var actionType in actionTypes)
{
var name = GetActionName(actionType);
var metadata = actionType.GetCustomAttribute<RuleActionAttribute>();
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<RuleActionHandlerAttribute>().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);
}
}
}

5
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 28'><path d='M23.734 12.125c1.281 0 2.266.938 2.266 2.219 0 1-.516 1.703-1.453 2.031l-2.688.922.875 2.609c.078.234.109.484.109.734 0 1.234-1 2.266-2.234 2.266a2.271 2.271 0 0 1-2.172-1.547l-.859-2.578-4.844 1.656.859 2.562c.078.234.125.484.125.734 0 1.219-1 2.266-2.25 2.266a2.25 2.25 0 0 1-2.156-1.547l-.859-2.547-2.391.828c-.25.078-.516.141-.781.141-1.266 0-2.219-.938-2.219-2.203 0-.969.625-1.844 1.547-2.156l2.438-.828-1.641-4.891-2.438.844c-.25.078-.5.125-.75.125-1.25 0-2.219-.953-2.219-2.203 0-.969.625-1.844 1.547-2.156l2.453-.828-.828-2.484a2.337 2.337 0 0 1-.125-.734c0-1.234 1-2.266 2.25-2.266a2.25 2.25 0 0 1 2.156 1.547l.844 2.5L13.14 5.5 12.296 3a2.337 2.337 0 0 1-.125-.734c0-1.234 1.016-2.266 2.25-2.266.984 0 1.859.625 2.172 1.547l.828 2.516 2.531-.859c.219-.063.438-.094.672-.094 1.219 0 2.266.906 2.266 2.156 0 .969-.75 1.781-1.625 2.078l-2.453.844 1.641 4.937 2.562-.875a2.32 2.32 0 0 1 .719-.125zm-12.406 4.094l4.844-1.641-1.641-4.922-4.844 1.672z'/></svg>",
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; }
}
}

21
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<SlackAction, SlackActionHandler>();
}
}
}

57
extensions/Squidex.Extensions/Actions/TriggerTypes.cs

@ -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<string, RuleElement> All = new Dictionary<string, RuleElement>
{
[GetTriggerName(typeof(ContentChangedTriggerV2))] = new RuleElement
{
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M21.875 28H6.125A6.087 6.087 0 0 1 0 21.875V6.125A6.087 6.087 0 0 1 6.125 0h15.75A6.087 6.087 0 0 1 28 6.125v15.75A6.088 6.088 0 0 1 21.875 28zM6.125 1.75A4.333 4.333 0 0 0 1.75 6.125v15.75a4.333 4.333 0 0 0 4.375 4.375h15.75a4.333 4.333 0 0 0 4.375-4.375V6.125a4.333 4.333 0 0 0-4.375-4.375H6.125z'/><path d='M13.125 12.25H7.35c-1.575 0-2.888-1.313-2.888-2.888V7.349c0-1.575 1.313-2.888 2.888-2.888h5.775c1.575 0 2.887 1.313 2.887 2.888v2.013c0 1.575-1.312 2.888-2.887 2.888zM7.35 6.212c-.613 0-1.138.525-1.138 1.138v2.012A1.16 1.16 0 0 0 7.35 10.5h5.775a1.16 1.16 0 0 0 1.138-1.138V7.349a1.16 1.16 0 0 0-1.138-1.138H7.35zM22.662 16.713H5.337c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h17.237c.525 0 .875.35.875.875s-.35.875-.787.875zM15.138 21.262h-9.8c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h9.713c.525 0 .875.35.875.875s-.35.875-.787.875z'/></svg>",
IconColor = "#3389ff",
Display = "Content changed",
Description = "For content changes like created, updated, published, unpublished..."
},
[GetTriggerName(typeof(AssetChangedTriggerV2))] = new RuleElement
{
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M21.875 28H6.125A6.087 6.087 0 0 1 0 21.875V6.125A6.087 6.087 0 0 1 6.125 0h15.75A6.087 6.087 0 0 1 28 6.125v15.75A6.088 6.088 0 0 1 21.875 28zM6.125 1.75A4.333 4.333 0 0 0 1.75 6.125v15.75a4.333 4.333 0 0 0 4.375 4.375h15.75a4.333 4.333 0 0 0 4.375-4.375V6.125a4.333 4.333 0 0 0-4.375-4.375H6.125z'/><path d='M21.088 23.537H9.1c-.35 0-.612-.175-.787-.525s-.088-.7.088-.962l8.225-9.713c.175-.175.438-.35.7-.35s.525.175.7.35l5.25 7.525c.088.087.088.175.088.262.438 1.225.087 2.012-.175 2.45-.613.875-1.925.963-2.1.963zm-10.063-1.75h10.15c.175 0 .612-.088.7-.262.088-.088.088-.35 0-.7l-4.55-6.475-6.3 7.438zM9.1 13.737c-2.1 0-3.85-1.75-3.85-3.85S7 6.037 9.1 6.037s3.85 1.75 3.85 3.85-1.663 3.85-3.85 3.85zm0-5.949c-1.138 0-2.1.875-2.1 2.1s.962 2.1 2.1 2.1 2.1-.962 2.1-2.1-.875-2.1-2.1-2.1z'/></svg>",
IconColor = "#3389ff",
Display = "Asset changed",
Description = "For asset changes like uploaded, updated (reuploaded), renamed, deleted..."
},
[GetTriggerName(typeof(SchemaChangedTrigger))] = new RuleElement
{
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M9.6 0c-.6 0-1 .4-1 1s.4 1 1 1h12.8c.6 0 1.1-.4 1.1-1s-.4-1-1-1H9.6zM6.1 4.3c-.6 0-1 .4-1 1s.4 1 1 1h19.8c.5 0 .9-.4.9-1s-.4-1-1-1H6.1zM7 8.6c-3.9 0-7 3.1-7 7V25c0 3.9 3.1 7 7 7h18c3.9 0 7-3.1 7-7v-9.4c0-3.9-3.1-7-7-7H7zm0 2h18c2.8 0 5 2.2 5 5V25c0 2.8-2.2 5-5 5H7c-2.8 0-5-2.2-5-5v-9.4c0-2.8 2.2-5 5-5zM5.3 13v2c0 2.4 2 4.4 4.4 4.4h12.7c2.4 0 4.4-2 4.4-4.4v-2H25v2c0 1.5-1.2 2.6-2.6 2.6H9.6C8.2 17.7 7 16.5 7 15v-2H5.3z' id='path5869'/></svg>",
IconColor = "#3389ff",
Display = "Schema changed",
Description = "When a schema definition has been created, updated, published or deleted..."
},
[GetTriggerName(typeof(UsageTrigger))] = new RuleElement
{
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M21.2 11.4c-.2 0-.4-.1-.6-.2-.5-.3-.6-.9-.3-1.4L22 7.2c.3-.5.9-.6 1.4-.3.6.4.7 1.1.4 1.5L22.1 11c-.2.3-.5.4-.9.4zM16 20.9h-.2c-1-.1-2-.6-2.5-1.5l-6-8.7c-.3-.3-.3-.8 0-1.2.3-.3.8-.4 1.2-.2l9.2 5.4c.9.5 1.5 1.4 1.6 2.4.1 1-.2 2-.9 2.8-.6.7-1.5 1-2.4 1zm-4.6-7.5l3.4 5c.2.3.6.6 1 .6s.8-.1 1.1-.4c.3-.3.4-.7.3-1.1-.1-.4-.3-.7-.6-1zM25.9 32H6.1C2.8 32 0 29.2 0 25.9v-10C0 7.1 7.1 0 15.8 0 24.8 0 32 7.2 32 16.2v9.7c0 3.3-2.8 6.1-6.1 6.1zM15.8 2C8.2 2 2 8.2 2 15.8v10C2 28.1 3.9 30 6.1 30h19.7c2.3 0 4.1-1.9 4.1-4.1v-9.7C30 8.4 23.6 2 15.8 2z'/></svg>",
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);
}
}
}

6
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M32 7.075a12.941 12.941 0 0 1-3.769 1.031 6.601 6.601 0 0 0 2.887-3.631 13.21 13.21 0 0 1-4.169 1.594A6.565 6.565 0 0 0 22.155 4a6.563 6.563 0 0 0-6.563 6.563c0 .512.056 1.012.169 1.494A18.635 18.635 0 0 1 2.23 5.195a6.56 6.56 0 0 0-.887 3.3 6.557 6.557 0 0 0 2.919 5.463 6.565 6.565 0 0 1-2.975-.819v.081a6.565 6.565 0 0 0 5.269 6.437 6.574 6.574 0 0 1-2.968.112 6.588 6.588 0 0 0 6.131 4.563 13.17 13.17 0 0 1-9.725 2.719 18.568 18.568 0 0 0 10.069 2.95c12.075 0 18.681-10.006 18.681-18.681 0-.287-.006-.569-.019-.85A13.216 13.216 0 0 0 32 7.076z'/></svg>",
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; }
}
}

24
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<TwitterOptions>(
config.GetSection("twitter"));
services.AddRuleAction<TweetAction, TweetActionHandler>();
}
}
}

9
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M5.95 27.125h-.262C1.75 26.425 0 23.187 0 20.3c0-2.713 1.575-5.688 5.075-6.563V9.712c0-.525.35-.875.875-.875s.875.35.875.875v4.725c0 .438-.35.787-.7.875-2.975.438-4.375 2.8-4.375 4.988s1.313 4.55 4.2 5.075h.175a.907.907 0 0 1 .7 1.05c-.088.438-.438.7-.875.7zM21.175 27.387c-2.8 0-5.775-1.662-6.65-5.075H9.712c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h5.512c.438 0 .787.35.875.7.438 2.975 2.8 4.288 4.988 4.375 2.188 0 4.55-1.313 5.075-4.2v-.088a.908.908 0 0 1 1.05-.7.908.908 0 0 1 .7 1.05v.088c-.612 3.85-3.85 5.6-6.737 5.6zM21.525 18.55c-.525 0-.875-.35-.875-.875v-4.813c0-.438.35-.787.7-.875 2.975-.438 4.288-2.8 4.375-4.987 0-2.188-1.313-4.55-4.2-5.075h-.088c-.525-.175-.875-.613-.787-1.05s.525-.788 1.05-.7h.088c3.938.7 5.688 3.937 5.688 6.825 0 2.713-1.662 5.688-5.075 6.563v4.113c0 .438-.438.875-.875.875zM1.137 6.737H.962c-.438-.087-.788-.525-.7-.963v-.087c.7-3.938 3.85-5.688 6.737-5.688h.087c2.712 0 5.688 1.662 6.563 5.075h4.025c.525 0 .875.35.875.875s-.35.875-.875.875h-4.725c-.438 0-.788-.35-.875-.7-.438-2.975-2.8-4.288-4.988-4.375-2.188 0-4.55 1.313-5.075 4.2v.087c-.088.438-.438.7-.875.7z'/><path d='M7 10.588c-.875 0-1.837-.35-2.538-1.05a3.591 3.591 0 0 1 0-5.075C5.162 3.851 6.037 3.5 7 3.5s1.838.35 2.537 1.05c.7.7 1.05 1.575 1.05 2.537s-.35 1.837-1.05 2.538c-.7.612-1.575.963-2.537.963zM7 5.25c-.438 0-.875.175-1.225.525a1.795 1.795 0 0 0 2.538 2.538c.35-.35.525-.788.525-1.313s-.175-.875-.525-1.225S7.525 5.25 7 5.25zM21.088 23.887a3.65 3.65 0 0 1-2.537-1.05 3.591 3.591 0 0 1 0-5.075c.7-.7 1.575-1.05 2.537-1.05s1.838.35 2.537 1.05c.7.7 1.05 1.575 1.05 2.538s-.35 1.837-1.05 2.537c-.787.7-1.662 1.05-2.537 1.05zm0-5.337c-.525 0-.963.175-1.313.525a1.795 1.795 0 0 0 2.537 2.538c.35-.35.525-.788.525-1.313s-.175-.963-.525-1.313-.787-.438-1.225-.438zM20.387 10.588c-.875 0-1.837-.35-2.537-1.05S16.8 7.963 16.8 7.001s.35-1.837 1.05-2.538c.7-.612 1.662-.962 2.537-.962s1.838.35 2.538 1.05c1.4 1.4 1.4 3.675 0 5.075-.7.612-1.575.963-2.538.963zm0-5.338c-.525 0-.962.175-1.313.525s-.525.788-.525 1.313.175.962.525 1.313c.7.7 1.838.7 2.538 0s.7-1.838 0-2.538c-.263-.438-.7-.612-1.225-.612zM7.087 23.887c-.875 0-1.837-.35-2.538-1.05s-1.05-1.575-1.05-2.537.35-1.838 1.05-2.538c.7-.612 1.575-.962 2.538-.962s1.837.35 2.538 1.05c1.4 1.4 1.4 3.675 0 5.075-.7.612-1.575.962-2.538.962zm0-5.337c-.525 0-.962.175-1.313.525s-.525.788-.525 1.313.175.963.525 1.313a1.794 1.794 0 1 0 2.538-2.537c-.263-.438-.7-.612-1.225-.612z'/></svg>",
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; }
}
}

21
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<WebhookAction, WebhookActionHandler>();
}
}
}

29
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<string>("assetStore:type");
if (string.Equals(storeType, "Memory", StringComparison.OrdinalIgnoreCase))
{
services.AddSingletonAs<MemoryAssetStore>()
.As<IAssetStore>();
}
}
}
}

6
extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -3,17 +3,13 @@
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="6.5.0" />
<PackageReference Include="Elasticsearch.Net" Version="6.6.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />

6
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;
}
}
}

4
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;
}

25
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<TAction, THandler>(this IServiceCollection services) where THandler : class, IRuleActionHandler where TAction : RuleAction
{
services.AddSingletonAs<THandler>()
.As<IRuleActionHandler>();
services.AddSingleton(new RuleActionRegistration(typeof(TAction)));
return services;
}
}
}

16
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
{
}
}

2
extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs → 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

7
extensions/Squidex.Extensions/Actions/RuleElement.cs → 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<RuleActionProperty> Properties { get; } = new List<RuleActionProperty>();
}
}

24
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; }
}
}

20
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
}
}

24
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;
}
}
}

140
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<string, RuleActionDefinition> actionTypes = new Dictionary<string, RuleActionDefinition>();
public IReadOnlyDictionary<string, RuleActionDefinition> Actions
{
get { return actionTypes; }
}
public RuleRegistry(IEnumerable<RuleActionRegistration> registrations = null)
{
if (registrations != null)
{
foreach (var registration in registrations)
{
Add(registration.ActionType);
}
}
}
public void Add<T>() where T : RuleAction
{
Add(typeof(T));
}
private void Add(Type actionType)
{
var metadata = actionType.GetCustomAttribute<RuleActionAttribute>();
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<DisplayAttribute>();
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<RequiredAttribute>() != null || (type.IsValueType && !IsNullable(type))) && type != typeof(bool) && type != typeof(bool?))
{
actionProperty.IsRequired = true;
}
if (property.GetCustomAttribute<FormattableAttribute>() != null)
{
actionProperty.IsFormattable = true;
}
var dataType = property.GetCustomAttribute<DataTypeAttribute>()?.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<Type>();
@ -39,8 +173,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules
typeNameRegistry.Map(type, type.Name);
}
}
return typeNameRegistry;
}
}
}

2
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -17,7 +17,7 @@
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1469" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.4" />
<PackageReference Include="NJsonSchema" Version="9.13.18" />
<PackageReference Include="NJsonSchema" Version="9.13.27" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" />

18
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;
}
}

2
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);
}

7
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<AssetState, Guid>
{
public async Task<(AssetState Value, long Version)> ReadAsync(Guid key)
async Task<(AssetState Value, long Version)> ISnapshotStore<AssetState, Guid>.ReadAsync(Guid key)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
@ -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<AssetState, Guid>.WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
@ -48,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
Task ISnapshotStore<AssetState, Guid>.ReadAllAsync(Func<AssetState, long, Task> callback)
Task ISnapshotStore<AssetState, Guid>.ReadAllAsync(Func<AssetState, long, Task> callback, CancellationToken ct)
{
throw new NotSupportedException();
}

167
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<MongoContentEntity>
{
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<MongoContentEntity> collection, CancellationToken ct = default)
protected override Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{
await collection.Indexes.CreateOneAsync(
new CreateIndexModel<MongoContentEntity>(Index.Ascending(x => x.ReferencedIds)), cancellationToken: ct);
return collection.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.ScheduledAt)
.Ascending(x => x.IsDeleted)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.ReferencedIds))
}, ct);
}
protected override string CollectionName()
{
return collectionName;
return "State_Contents";
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, Status[] status = null, bool useDraft = false)
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, List<Guid> 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<IContentEntity>(contentCount.Result, contentItems.Result);
@ -90,14 +107,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> ids, Status[] status = null)
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> 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<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<Guid> ids, Status[] status = null)
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<Guid> 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<IContentEntity>(contentCount.Result, contentItems.Result);
}
public async Task<IContentEntity> 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<IContentEntity, Task> 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<IReadOnlyList<Guid>> 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<IReadOnlyList<Guid>> 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<Guid, Guid, Task<ISchemaEntity>> 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<ContentState, long, Task> callback, Func<Guid, Guid, Task<ISchemaEntity>> 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;
}
}
}
}
}

152
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs

@ -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<MongoContentEntity> collection, CancellationToken ct = default)
{
await collection.Indexes.CreateManyAsync(
new[]
{
new CreateIndexModel<MongoContentEntity>(
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.Id)
.Ascending(x => x.IsDeleted)),
new CreateIndexModel<MongoContentEntity>(
Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.Id)
.Ascending(x => x.IsDeleted)),
new CreateIndexModel<MongoContentEntity>(
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<IReadOnlyList<Guid>> 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<IReadOnlyList<Guid>> 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<IContentEntity, Task> 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<IContentEntity> 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<Guid, Guid, Task<ISchemaEntity>> 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;
}
}
}
}
}

4
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<Guid> AppId { get; set; }

71
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs

@ -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<MongoContentEntity> collection, CancellationToken ct = default)
{
await collection.Indexes.CreateManyAsync(
new[]
{
new CreateIndexModel<MongoContentEntity>(
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(
Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(
Index
.Text(x => x.DataText)
.Ascending(x => x.IndexedSchemaId))
}, ct);
await base.SetupCollectionAsync(collection, ct);
}
public async Task<IContentEntity> 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 });
}
}
}

104
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<IResultList<IContentEntity>> 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<MongoContentRepository>("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<IContentEntity>(0);
}
return await contents.QueryAsync(app, schema, query, fullTextIds, status, true);
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(status, nameof(status));
Guard.NotNull(ids, nameof(ids));
using (Profiler.TraceMethod<MongoContentRepository>("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<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(status, nameof(status));
Guard.NotNull(ids, nameof(ids));
using (Profiler.TraceMethod<MongoContentRepository>("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<IContentEntity> 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<MongoContentRepository>())
{
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<MongoContentRepository>())
{
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<MongoContentRepository>())
{
return await contentsDraft.QueryIdsAsync(appId);
return await contents.QueryIdsAsync(appId);
}
}
@ -131,22 +136,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
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;
}
}
}

8
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()

42
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<ContentState, Guid>
{
public async Task<(ContentState Value, long Version)> ReadAsync(Guid key)
async Task ISnapshotStore<ContentState, Guid>.RemoveAsync(Guid key)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
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<ContentState, Guid>.ReadAllAsync(Func<ContentState, long, Task> callback, CancellationToken ct)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
await contents.ReadAllAsync(callback, GetSchemaAsync, ct);
}
}
async Task<(ContentState Value, long Version)> ISnapshotStore<ContentState, Guid>.ReadAsync(Guid key)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await contents.ReadAsync(key, GetSchemaAsync);
}
}
async Task ISnapshotStore<ContentState, Guid>.WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
@ -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<ContentState, Guid>.RemoveAsync(Guid key)
{
throw new NotSupportedException();
}
Task ISnapshotStore<ContentState, Guid>.ReadAllAsync(Func<ContentState, long, Task> callback)
{
throw new NotSupportedException();
}
}
}

59
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs → 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<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
@ -109,33 +110,65 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return cursor.Skip(query);
}
public static FilterDefinition<MongoContentEntity> ToFilter(this Query query, Guid schemaId, Status[] status)
public static IFindFluent<MongoContentEntity, MongoContentEntity> WithoutDraft(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, bool useDraft)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
return !useDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor;
}
public static FilterDefinition<MongoContentEntity> Build(Guid schemaId, Guid id, Status[] status)
{
return CreateFilter(null, schemaId, new List<Guid> { id }, status, null);
}
public static FilterDefinition<MongoContentEntity> IdsByApp(Guid appId, ICollection<Guid> ids, Status[] status)
{
return CreateFilter(appId, null, ids, status, null);
}
public static FilterDefinition<MongoContentEntity> IdsBySchema(Guid schemaId, ICollection<Guid> ids, Status[] status)
{
return CreateFilter(null, schemaId, ids, status, null);
}
public static FilterDefinition<MongoContentEntity> ToFilter(this Query query, Guid schemaId, ICollection<Guid> ids, Status[] status)
{
return CreateFilter(null, schemaId, ids, status, query);
}
private static FilterDefinition<MongoContentEntity> CreateFilter(Guid? appId, Guid? schemaId, ICollection<Guid> ids, Status[] status, Query query)
{
var filters = new List<FilterDefinition<MongoContentEntity>>();
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<MongoContentEntity>();
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<MongoContentEntity>());
}
return Filter.And(filters);
}

12
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<HistoryEvent>, IHistoryEventRepository
{
public MongoHistoryEventRepository(IMongoDatabase database)
public MongoHistoryEventRepository(IMongoDatabase database, IOptions<MongoDbOptions> options)
: base(database)
{
if (options.Value.IsCosmosDb)
{
var classMap = BsonClassMap.RegisterClassMap<HistoryEvent>();
classMap.MapProperty(x => x.Created)
.SetElementName("_ts");
classMap.AutoMap();
}
}
protected override string CollectionName()

2
src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -17,7 +17,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.5.4" />
<PackageReference Include="MongoDB.Driver" Version="2.7.3" />
<PackageReference Include="MongoDB.Driver" Version="2.8.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

5
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<IEvent> @event)
{
return Snapshot.Apply(@event);
}
public Task<J<IAppEntity>> GetStateAsync()
{
return J.AsTask<IAppEntity>(Snapshot);

16
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs

@ -40,11 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
public Task<bool> 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);

2
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<IEvent> @event)
public override AppState Apply(Envelope<IEvent> @event)
{
var payload = (SquidexEvent)@event.Payload;

5
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -165,11 +165,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
}
protected override AssetState OnEvent(Envelope<IEvent> @event)
{
return Snapshot.Apply(@event);
}
public Task<J<IAssetEntity>> GetStateAsync(long version = EtagVersion.Any)
{
return J.AsTask<IAssetEntity>(GetSnapshot(version));

4
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<AssetState, AssetGrain>(id, (e, s) => s.Apply(e)));
await RebuildManyAsync(assetIds, id => RebuildAsync<AssetState, AssetGrain>(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)
{

2
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<IEvent> @event)
public override AssetState Apply(Envelope<IEvent> @event)
{
var payload = (SquidexEvent)@event.Payload;

2
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;

5
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<TState, TGrain>(Guid key, Func<Envelope<IEvent>, TState, TState> func) where TState : IDomainState, new()
protected async Task RebuildAsync<TState, TGrain>(Guid key) where TState : IDomainState<TState>, 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++;
});

6
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<CommentsState>
{
public override CommentsState Apply(Envelope<IEvent> @event)
{
return this;
}
}
}

2
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<ContentState, ContentGrain>(id, (e, s) => s.Apply(e)));
return RebuildManyAsync(contentIds, id => RebuildAsync<ContentState, ContentGrain>(id));
}
}
}

5
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -296,11 +296,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
protected override ContentState OnEvent(Envelope<IEvent> @event)
{
return Snapshot.Apply(@event);
}
private async Task<ContentOperationContext> CreateContext(Guid appId, Guid schemaId, Guid contentId, Func<string> message)
{
var operationContext =

18
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<IContentRepository> contentRepository;
private readonly Lazy<ICommandBus> commandBus;
private readonly IContentRepository contentRepository;
private readonly ICommandBus commandBus;
private readonly IClock clock;
private readonly ISemanticLog log;
private TaskScheduler scheduler;
public ContentSchedulerGrain(
Lazy<IContentRepository> contentRepository,
Lazy<ICommandBus> 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)

2
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -30,7 +30,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id);
Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback);
Task RemoveAsync(Guid appId);
}
}

2
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<IEvent> @event)
public override ContentState Apply(Envelope<IEvent> @event)
{
var payload = (SquidexEvent)@event.Payload;

105
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<ITextIndexerGrain>(schemaId);
using (Profiler.TraceMethod<GrainTextIndexer>())
{
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<ITextIndexerGrain>(schemaId);
using (Profiler.TraceMethod<GrainTextIndexer>())
{
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<List<Guid>> SearchAsync(string queryText, IAppEntity app, Guid schemaId, bool useDraft = false)
{
if (string.IsNullOrWhiteSpace(queryText))
{
return null;
}
var index = grainFactory.GetGrain<ITextIndexerGrain>(schemaId);
using (Profiler.TraceMethod<GrainTextIndexer>())
{
var context = CreateContext(app, useDraft);
return await index.SearchAsync(queryText, context);
}
}
private static SearchContext CreateContext(IAppEntity app, bool useDraft)
{
var languages = new HashSet<string>(app.LanguagesConfig.Select(x => x.Key));
return new SearchContext { Languages = languages, IsDraft = useDraft };
}
}
}

24
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<List<Guid>> SearchAsync(string queryText, IAppEntity app, Guid schemaId, bool useDraft = false);
}
}

24
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<IndexData> data);
Task<List<Guid>> SearchAsync(string queryText, SearchContext context);
}
}

18
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; }
}
}

65
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<string, Analyzer> analyzers = new Dictionary<string, Analyzer>(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;
}
}
}
}

94
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;
}
}
}
}
}

18
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<string> Languages { get; set; }
}
}

297
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<string> 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<IndexData> 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<string, StringBuilder>();
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<string>(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<string> 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<List<Guid>> SearchAsync(string queryText, SearchContext context)
{
var result = new HashSet<Guid>();
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;
}
}
}

5
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<T> : Cloneable<T>,
IDomainState,
IDomainState<T>,
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<IEvent> @event);
public T Clone()
{
return Clone(x => { });

5
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -123,11 +123,6 @@ namespace Squidex.Domain.Apps.Entities.Rules
}
}
protected override RuleState OnEvent(Envelope<IEvent> @event)
{
return Snapshot.Apply(@event);
}
public Task<J<IRuleEntity>> GetStateAsync()
{
return J.AsTask<IRuleEntity>(Snapshot);

2
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<IEvent> @event)
public override RuleState Apply(Envelope<IEvent> @event)
{
var payload = (SquidexEvent)@event.Payload;

5
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -381,11 +381,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas
}
}
protected override SchemaState OnEvent(Envelope<IEvent> @event)
{
return Snapshot.Apply(@event);
}
public Task<J<ISchemaEntity>> GetStateAsync()
{
return J.AsTask<ISchemaEntity>(Snapshot);

2
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<IEvent> @event)
public override SchemaState Apply(Envelope<IEvent> @event)
{
var payload = (SquidexEvent)@event.Payload;

8
src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -16,11 +16,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.2.3">
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00005" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00005" />
<PackageReference Include="Lucene.Net.Queries" Version="4.8.0-beta00005" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00005" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.3.0" />
<PackageReference Include="NodaTime" Version="2.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

2
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities
{
public abstract class SquidexDomainObjectGrain<T> : DomainObjectGrain<T> where T : IDomainState, new()
public abstract class SquidexDomainObjectGrain<T> : DomainObjectGrain<T> where T : IDomainState<T>, new()
{
protected SquidexDomainObjectGrain(IStore<Guid> store, ISemanticLog log)
: base(store, log)

2
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities
{
public abstract class SquidexDomainObjectGrainLogSnapshots<T> : LogSnapshotDomainObjectGrain<T> where T : IDomainState, new()
public abstract class SquidexDomainObjectGrainLogSnapshots<T> : LogSnapshotDomainObjectGrain<T> where T : IDomainState<T>, new()
{
protected SquidexDomainObjectGrainLogSnapshots(IStore<Guid> store, ISemanticLog log)
: base(store, log)

2
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

4
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;
}

4
src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -14,10 +14,10 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="2.3.2" />
<PackageReference Include="IdentityServer4" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.3" />
<PackageReference Include="MongoDB.Driver" Version="2.8.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.5.1" />

10
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)
{

32
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<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
await documentClient.ReadDatabaseFeedAsync();
return HealthCheckResult.Healthy("Application must query data from CosmosDB.");
}
}
}

16
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";
}
}

33
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);
}
}
}

33
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; }
}
}

124
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<IncludedPath>
{
new IncludedPath
{
Path = "/*",
Indexes = new Collection<Index>
{
Index.Range(DataType.Number),
Index.Range(DataType.String),
}
}
}
},
UniqueKeyPolicy = new UniqueKeyPolicy
{
UniqueKeys = new Collection<UniqueKey>
{
new UniqueKey
{
Paths = new Collection<string>
{
$"/eventStream",
$"/eventStreamOffset"
}
}
}
},
Id = Constants.Collection,
},
new RequestOptions
{
PartitionKey = new PartitionKey($"/eventStream")
});
}
}
}

142
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<IReadOnlyList<StoredEvent>> QueryAsync(string streamName, long streamPosition = 0)
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
ThrowIfDisposed();
using (Profiler.TraceMethod<CosmosDbEventStore>())
{
var query = FilterBuilder.ByStreamName(streamName, streamPosition - MaxCommitSize);
var result = new List<StoredEvent>();
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<StoredEvent, Task> 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<StoredEvent, Task> 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<StoredEvent, Task> callback, StreamPosition lastPosition, SqlQuerySpec query, EventPredicate filterExpression, CancellationToken ct = default)
{
using (Profiler.TraceMethod<CosmosDbEventStore>())
{
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);
}
}
}
}

149
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<EventData> events)
{
return AppendAsync(commitId, streamName, EtagVersion.Any, events);
}
public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection<EventData> 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<CosmosDbEventStore>())
{
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<long> GetEventStreamOffsetAsync(string streamName)
{
var query =
documentClient.CreateDocumentQuery<CosmosDbEventCommit>(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<EventData> 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;
}
}
}

150
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<bool> processorStopRequested = new TaskCompletionSource<bool>();
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<Document> docs, CancellationToken cancellationToken)
{
if (!processorStopRequested.Task.IsCompleted)
{
foreach (var document in docs)
{
if (!processorStopRequested.Task.IsCompleted)
{
var streamName = document.GetPropertyValue<string>("eventStream");
if (regex == null || regex.IsMatch(streamName))
{
var commit = JsonConvert.DeserializeObject<CosmosDbEventCommit>(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;
}
}
}

156
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<string>();
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<string>();
var parameters = new SqlParameterCollection();
filters.ForPosition(parameters, streamPosition);
filters.ForRegex(parameters, streamFilter);
return BuildQuery(filters, parameters);
}
private static SqlQuerySpec BuildQuery(List<string> 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<string> 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<string> 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<string> 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;
}
}
}
}

62
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<T> FirstOrDefaultAsync<T>(this IQueryable<T> queryable, CancellationToken ct = default)
{
var documentQuery = queryable.AsDocumentQuery();
using (documentQuery)
{
if (documentQuery.HasMoreResults)
{
var results = await documentQuery.ExecuteNextAsync<T>(ct);
return results.FirstOrDefault();
}
}
return default;
}
public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func<CosmosDbEventCommit, Task> handler, CancellationToken ct = default)
{
var query = documentClient.CreateDocumentQuery<CosmosDbEventCommit>(collectionUri, querySpec);
return query.QueryAsync(handler, ct);
}
public static async Task QueryAsync<T>(this IQueryable<T> queryable, Func<T, Task> handler, CancellationToken ct = default)
{
var documentQuery = queryable.AsDocumentQuery();
using (documentQuery)
{
while (documentQuery.HasMoreResults && !ct.IsCancellationRequested)
{
var items = await documentQuery.ExecuteNextAsync<T>(ct);
foreach (var item in items)
{
await handler(item);
}
}
}
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save