Browse Source

Feature/full delete (#765)

* Started with a few repositories.

* Get rid unnecessary allocations.

* New repositories and cancellation token improvements.

* First untested implementation.

* Fix infrastructure tests.

* Warnings fixed.

* All tests green again.

* New tests.

* Better cancellation.

* More tests.

* Backup fix.

* Markdown everything

* A few more tests and fixes.

* Fix app deletion flag.

* Provide app to client creator.

* Started with migration.

* Temp.

* Minor change to rename app deletion.

* Toggle for deleter.

* Fix build.
pull/768/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
4d9fbd7322
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 69
      backend/.editorconfig
  2. 2
      backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs
  3. 2
      backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs
  4. 2
      backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs
  5. 2
      backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs
  6. 3
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
  7. 5
      backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs
  8. 3
      backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs
  9. 3
      backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs
  10. 3
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
  11. 3
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  12. 3
      backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs
  13. 3
      backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs
  14. 5
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs
  15. 8
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs
  16. 3
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs
  17. 3
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs
  18. 3
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs
  19. 3
      backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs
  20. 3
      backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs
  21. 3
      backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs
  22. 3
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs
  23. 3
      backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs
  24. 6
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
  25. 2
      backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs
  26. 2
      backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs
  27. 4
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  28. 12
      backend/i18n/frontend_en.json
  29. 10
      backend/i18n/frontend_it.json
  30. 10
      backend/i18n/frontend_nl.json
  31. 10
      backend/i18n/frontend_zh.json
  32. 12
      backend/i18n/source/frontend_en.json
  33. 5
      backend/i18n/source/frontend_it.json
  34. 5
      backend/i18n/source/frontend_nl.json
  35. 5
      backend/i18n/source/frontend_zh.json
  36. 23
      backend/src/Migrations/MigrationPath.cs
  37. 4
      backend/src/Migrations/Migrations.csproj
  38. 3
      backend/src/Migrations/Migrations/ClearRules.cs
  39. 3
      backend/src/Migrations/Migrations/ClearSchemas.cs
  40. 3
      backend/src/Migrations/Migrations/ConvertEventStore.cs
  41. 3
      backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs
  42. 9
      backend/src/Migrations/Migrations/CreateAssetSlugs.cs
  43. 20
      backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs
  44. 30
      backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs
  45. 5
      backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs
  46. 3
      backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs
  47. 3
      backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs
  48. 3
      backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs
  49. 3
      backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs
  50. 7
      backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs
  51. 189
      backend/src/Migrations/Migrations/PopulateGrainIndexes.cs
  52. 3
      backend/src/Migrations/Migrations/RebuildApps.cs
  53. 3
      backend/src/Migrations/Migrations/RebuildAssetFolders.cs
  54. 3
      backend/src/Migrations/Migrations/RebuildAssets.cs
  55. 3
      backend/src/Migrations/Migrations/RebuildContents.cs
  56. 34
      backend/src/Migrations/Migrations/RebuildRules.cs
  57. 34
      backend/src/Migrations/Migrations/RebuildSchemas.cs
  58. 3
      backend/src/Migrations/Migrations/RebuildSnapshots.cs
  59. 3
      backend/src/Migrations/Migrations/StartEventConsumers.cs
  60. 3
      backend/src/Migrations/Migrations/StopEventConsumers.cs
  61. 24
      backend/src/Migrations/OldEvents/AppArchived.cs
  62. 5
      backend/src/Migrations/OldEvents/AppClientChanged.cs
  63. 14
      backend/src/Migrations/RebuildRunner.cs
  64. 18
      backend/src/Migrations/RebuilderExtensions.cs
  65. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs
  66. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  67. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs
  68. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs
  69. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
  70. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  71. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  72. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs
  73. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs
  74. 9
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs
  75. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs
  76. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs
  77. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs
  78. 11
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  79. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  80. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  81. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  82. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
  83. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  84. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
  85. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs
  86. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  87. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs
  88. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs
  89. 46
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs
  90. 86
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs
  91. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs
  92. 43
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs
  93. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  94. 43
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  95. 44
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  96. 15
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  97. 74
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  98. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs
  99. 9
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs
  100. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs

69
backend/.editorconfig

@ -34,6 +34,75 @@ dotnet_diagnostic.IDE0066.severity = none
# IDE0090: Use 'new(...)' # IDE0090: Use 'new(...)'
dotnet_diagnostic.IDE0090.severity = none dotnet_diagnostic.IDE0090.severity = none
# MA0002: IEqualityComparer<string> or IComparer<string> is missing
dotnet_diagnostic.MA0002.severity = none
# MA0003: Add argument name to improve readability
dotnet_diagnostic.MA0003.severity = none
# MA0004: Use Task.ConfigureAwait(false)
dotnet_diagnostic.MA0004.severity = none
# MA0006: Use String.Equals instead of equality operator
dotnet_diagnostic.MA0006.severity = none
# MA0007: Add a comma after the last value
dotnet_diagnostic.MA0007.severity = none
# MA0008: Add StructLayoutAttribute
dotnet_diagnostic.MA0008.severity = none
# MA0009: Add regex evaluation timeout
dotnet_diagnostic.MA0009.severity = none
# MA0016: Prefer return collection abstraction instead of implementation
dotnet_diagnostic.MA0016.severity = none
# MA0018: Do not declare static members on generic types
dotnet_diagnostic.MA0018.severity = none
# MA0025: Implement the functionality instead of throwing NotImplementedException
dotnet_diagnostic.MA0025.severity = none
# MA0028: Optimize StringBuilder usage
dotnet_diagnostic.MA0028.severity = none
# MA0029: Combine LINQ methods
dotnet_diagnostic.MA0029.severity = none
# MA0031: Optimize Enumerable.Count() usage
dotnet_diagnostic.MA0031.severity = none
# MA0036: Make class static
dotnet_diagnostic.MA0036.severity = none
# MA0038: Make method static
dotnet_diagnostic.MA0038.severity = none
# MA0039: Do not write your own certificate validation method
dotnet_diagnostic.MA0039.severity = none
# MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = none
# MA0049: Type name should not match containing namespace
dotnet_diagnostic.MA0049.severity = none
# MA0051: Method is too long
dotnet_diagnostic.MA0051.severity = none
# MA0069: Non-constant static fields should not be visible
dotnet_diagnostic.MA0069.severity = none
# MA0071: Avoid using redundant else
dotnet_diagnostic.MA0071.severity = none
# MA0076: Do not use implicit culture-sensitive ToString in interpolated strings
dotnet_diagnostic.MA0076.severity = none
# MA0097: A class that implements IComparable<T> or IComparable should override comparison operators
dotnet_diagnostic.MA0097.severity = none
# RECS0129: Removes 'internal' modifiers that are not required # RECS0129: Removes 'internal' modifiers that are not required
dotnet_diagnostic.RECS0129.severity = none dotnet_diagnostic.RECS0129.severity = none

2
backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs

@ -16,7 +16,7 @@ namespace Squidex.Extensions.APM.ApplicationInsights
{ {
public sealed class ApplicationInsightsPlugin : IPlugin public sealed class ApplicationInsightsPlugin : IPlugin
{ {
private class Configurator : ITelemetryConfigurator private sealed class Configurator : ITelemetryConfigurator
{ {
private readonly IConfiguration config; private readonly IConfiguration config;

2
backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs

@ -16,7 +16,7 @@ namespace Squidex.Extensions.APM.Datadog
{ {
public sealed class OtlpPlugin : IPlugin public sealed class OtlpPlugin : IPlugin
{ {
private class Configurator : ITelemetryConfigurator private sealed class Configurator : ITelemetryConfigurator
{ {
private readonly IConfiguration config; private readonly IConfiguration config;

2
backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs

@ -30,7 +30,7 @@ namespace Squidex.Extensions.APM.Stackdriver
public string GetHttpMethod() public string GetHttpMethod()
{ {
return httpContextAccessor.HttpContext?.Request?.Method?.ToString() ?? string.Empty; return httpContextAccessor.HttpContext?.Request?.Method ?? string.Empty;
} }
public string GetUri() public string GetUri()

2
backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs

@ -18,7 +18,7 @@ namespace Squidex.Extensions.APM.Stackdriver
{ {
public sealed class StackdriverPlugin : IPlugin public sealed class StackdriverPlugin : IPlugin
{ {
private class Configurator : ITelemetryConfigurator private sealed class Configurator : ITelemetryConfigurator
{ {
private readonly string projectId; private readonly string projectId;

3
backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs

@ -94,7 +94,8 @@ namespace Squidex.Extensions.Actions.Algolia
return ("Ignore", new AlgoliaJob()); return ("Ignore", new AlgoliaJob());
} }
protected override async Task<Result> ExecuteJobAsync(AlgoliaJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(AlgoliaJob job,
CancellationToken ct = default)
{ {
if (string.IsNullOrWhiteSpace(job.AppId)) if (string.IsNullOrWhiteSpace(job.AppId))
{ {

5
backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -58,7 +58,8 @@ namespace Squidex.Extensions.Actions.AzureQueue
return (ruleDescription, ruleJob); return (ruleDescription, ruleJob);
} }
protected override async Task<Result> ExecuteJobAsync(AzureQueueJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(AzureQueueJob job,
CancellationToken ct = default)
{ {
var queue = await clients.GetClientAsync((job.QueueConnectionString, job.QueueName)); var queue = await clients.GetClientAsync((job.QueueConnectionString, job.QueueName));

3
backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs

@ -54,7 +54,8 @@ namespace Squidex.Extensions.Actions.Comment
return ("Ignore", new CreateComment()); return ("Ignore", new CreateComment());
} }
protected override async Task<Result> ExecuteJobAsync(CreateComment job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(CreateComment job,
CancellationToken ct = default)
{ {
if (job.CommentsId == default) if (job.CommentsId == default)
{ {

3
backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs

@ -72,7 +72,8 @@ namespace Squidex.Extensions.Actions.CreateContent
return (Description, ruleJob); return (Description, ruleJob);
} }
protected override async Task<Result> ExecuteJobAsync(Command job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(Command job,
CancellationToken ct = default)
{ {
var command = job; var command = job;

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

@ -65,7 +65,8 @@ namespace Squidex.Extensions.Actions.Discourse
return (description, ruleJob); return (description, ruleJob);
} }
protected override async Task<Result> ExecuteJobAsync(DiscourseJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(DiscourseJob job,
CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) using (var httpClient = httpClientFactory.CreateClient())
{ {

3
backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs

@ -100,7 +100,8 @@ namespace Squidex.Extensions.Actions.ElasticSearch
return ("Ignore", new ElasticSearchJob()); return ("Ignore", new ElasticSearchJob());
} }
protected override async Task<Result> ExecuteJobAsync(ElasticSearchJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(ElasticSearchJob job,
CancellationToken ct = default)
{ {
if (string.IsNullOrWhiteSpace(job.ServerHost)) if (string.IsNullOrWhiteSpace(job.ServerHost))
{ {

3
backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs

@ -41,7 +41,8 @@ namespace Squidex.Extensions.Actions.Email
return (description, ruleJob); return (description, ruleJob);
} }
protected override async Task<Result> ExecuteJobAsync(EmailJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(EmailJob job,
CancellationToken ct = default)
{ {
using (var smtpClient = new SmtpClient()) using (var smtpClient = new SmtpClient())
{ {

3
backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs

@ -46,7 +46,8 @@ namespace Squidex.Extensions.Actions.Fastly
return (Description, ruleJob); return (Description, ruleJob);
} }
protected override async Task<Result> ExecuteJobAsync(FastlyJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(FastlyJob job,
CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) using (var httpClient = httpClientFactory.CreateClient())
{ {

5
backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs

@ -74,7 +74,7 @@ namespace Squidex.Extensions.Actions.Kafka
foreach (var line in lines) foreach (var line in lines)
{ {
var indexEqual = line.IndexOf('='); var indexEqual = line.IndexOf('=', StringComparison.Ordinal);
if (indexEqual > 0 && indexEqual < line.Length - 1) if (indexEqual > 0 && indexEqual < line.Length - 1)
{ {
@ -90,7 +90,8 @@ namespace Squidex.Extensions.Actions.Kafka
return headersDictionary; return headersDictionary;
} }
protected override async Task<Result> ExecuteJobAsync(KafkaJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(KafkaJob job,
CancellationToken ct = default)
{ {
try try
{ {

8
backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs

@ -112,7 +112,8 @@ namespace Squidex.Extensions.Actions.Kafka
.WriteProperty("reason", error.Reason)); .WriteProperty("reason", error.Reason));
} }
public async Task SendAsync(KafkaJob job, CancellationToken ct) public async Task SendAsync(KafkaJob job,
CancellationToken ct)
{ {
if (!string.IsNullOrWhiteSpace(job.Schema)) if (!string.IsNullOrWhiteSpace(job.Schema))
{ {
@ -130,7 +131,8 @@ namespace Squidex.Extensions.Actions.Kafka
} }
} }
private static async Task ProduceAsync<T>(IProducer<string, T> producer, Message<string, T> message, KafkaJob job, CancellationToken ct) private static async Task ProduceAsync<T>(IProducer<string, T> producer, Message<string, T> message, KafkaJob job,
CancellationToken ct)
{ {
message.Key = job.MessageKey; message.Key = job.MessageKey;
@ -146,7 +148,7 @@ namespace Squidex.Extensions.Actions.Kafka
if (!string.IsNullOrWhiteSpace(job.PartitionKey) && job.PartitionCount > 0) if (!string.IsNullOrWhiteSpace(job.PartitionKey) && job.PartitionCount > 0)
{ {
var partition = Math.Abs(job.PartitionKey.GetHashCode()) % job.PartitionCount; var partition = Math.Abs(job.PartitionKey.GetHashCode(StringComparison.Ordinal)) % job.PartitionCount;
await producer.ProduceAsync(new TopicPartition(job.TopicName, partition), message, ct); await producer.ProduceAsync(new TopicPartition(job.TopicName, partition), message, ct);
} }

3
backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs

@ -79,7 +79,8 @@ namespace Squidex.Extensions.Actions.Medium
} }
} }
protected override async Task<Result> ExecuteJobAsync(MediumJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(MediumJob job,
CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) using (var httpClient = httpClientFactory.CreateClient())
{ {

3
backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs

@ -72,7 +72,8 @@ namespace Squidex.Extensions.Actions.Notification
return ("Ignore", new CreateComment()); return ("Ignore", new CreateComment());
} }
protected override async Task<Result> ExecuteJobAsync(CreateComment job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(CreateComment job,
CancellationToken ct = default)
{ {
if (job.CommentsId == default) if (job.CommentsId == default)
{ {

3
backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs

@ -34,7 +34,8 @@ namespace Squidex.Extensions.Actions.Prerender
return ($"Recache {url}", new PrerenderJob { RequestBody = requestBody }); return ($"Recache {url}", new PrerenderJob { RequestBody = requestBody });
} }
protected override async Task<Result> ExecuteJobAsync(PrerenderJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(PrerenderJob job,
CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) using (var httpClient = httpClientFactory.CreateClient())
{ {

3
backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs

@ -45,7 +45,8 @@ namespace Squidex.Extensions.Actions
return @event is EnrichedAssetEvent { Type: EnrichedAssetEventType.Deleted }; return @event is EnrichedAssetEvent { Type: EnrichedAssetEventType.Deleted };
} }
public static async Task<Result> OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null, CancellationToken ct = default) public static async Task<Result> OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null,
CancellationToken ct = default)
{ {
HttpResponseMessage response = null; HttpResponseMessage response = null;
try try

3
backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs

@ -30,7 +30,8 @@ namespace Squidex.Extensions.Actions.Script
return Task.FromResult(($"Run a script", job)); return Task.FromResult(($"Run a script", job));
} }
protected override async Task<Result> ExecuteJobAsync(ScriptJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(ScriptJob job,
CancellationToken ct = default)
{ {
var vars = new ScriptVars var vars = new ScriptVars
{ {

3
backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs

@ -67,7 +67,8 @@ namespace Squidex.Extensions.Actions.SignalR
return (ruleDescription, ruleJob); return (ruleDescription, ruleJob);
} }
protected override async Task<Result> ExecuteJobAsync(SignalRJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(SignalRJob job,
CancellationToken ct = default)
{ {
var signalR = await clients.GetClientAsync((job.ConnectionString, job.HubName)); var signalR = await clients.GetClientAsync((job.ConnectionString, job.HubName));

3
backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs

@ -40,7 +40,8 @@ namespace Squidex.Extensions.Actions.Slack
return (Description, ruleJob); return (Description, ruleJob);
} }
protected override async Task<Result> ExecuteJobAsync(SlackJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(SlackJob job,
CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) using (var httpClient = httpClientFactory.CreateClient())
{ {

3
backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs

@ -39,7 +39,8 @@ namespace Squidex.Extensions.Actions.Twitter
return (Description, ruleJob); return (Description, ruleJob);
} }
protected override async Task<Result> ExecuteJobAsync(TweetJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(TweetJob job,
CancellationToken ct = default)
{ {
var tokens = Tokens.Create( var tokens = Tokens.Create(
twitterOptions.ClientId, twitterOptions.ClientId,

6
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
@ -73,7 +74,7 @@ namespace Squidex.Extensions.Actions.Webhook
foreach (var line in lines) foreach (var line in lines)
{ {
var indexEqual = line.IndexOf('='); var indexEqual = line.IndexOf('=', StringComparison.Ordinal);
if (indexEqual > 0 && indexEqual < line.Length - 1) if (indexEqual > 0 && indexEqual < line.Length - 1)
{ {
@ -89,7 +90,8 @@ namespace Squidex.Extensions.Actions.Webhook
return headersDictionary; return headersDictionary;
} }
protected override async Task<Result> ExecuteJobAsync(WebhookJob job, CancellationToken ct = default) protected override async Task<Result> ExecuteJobAsync(WebhookJob job,
CancellationToken ct = default)
{ {
using (var httpClient = httpClientFactory.CreateClient()) using (var httpClient = httpClientFactory.CreateClient())
{ {

2
backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs

@ -56,7 +56,7 @@ namespace Squidex.Extensions.Assets.Azure
{ {
if (command.Type == AssetType.Image && command.File.FileSize <= MaxSize) if (command.Type == AssetType.Image && command.File.FileSize <= MaxSize)
{ {
using (var stream = command.File.OpenRead()) await using (var stream = command.File.OpenRead())
{ {
var result = await client.AnalyzeImageInStreamAsync(stream, features); var result = await client.AnalyzeImageInStreamAsync(stream, features);

2
backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs

@ -24,7 +24,7 @@ namespace Squidex.Extensions.Samples.AssetStore
{ {
builder.Use(async (context, next) => builder.Use(async (context, next) =>
{ {
if (context.Request.Path.StartsWithSegments("/api/assets/memory")) if (context.Request.Path.StartsWithSegments("/api/assets/memory", StringComparison.Ordinal))
{ {
context.Response.StatusCode = 200; context.Response.StatusCode = 200;

4
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -16,6 +16,10 @@
<PackageReference Include="Elasticsearch.Net" Version="7.14.1" /> <PackageReference Include="Elasticsearch.Net" Version="7.14.1" />
<PackageReference Include="Google.Cloud.Diagnostics.Common" Version="4.0.0" /> <PackageReference Include="Google.Cloud.Diagnostics.Common" Version="4.0.0" />
<PackageReference Include="Google.Cloud.Logging.V2" Version="3.3.0" /> <PackageReference Include="Google.Cloud.Logging.V2" Version="3.3.0" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.670">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Azure.CognitiveServices.Vision.ComputerVision" Version="7.0.0" /> <PackageReference Include="Microsoft.Azure.CognitiveServices.Vision.ComputerVision" Version="7.0.0" />
<PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.9.2" /> <PackageReference Include="Microsoft.Azure.SignalR.Management" Version="1.9.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />

12
backend/i18n/frontend_en.json

@ -12,11 +12,6 @@
"apps.appNameWarning": "The app name cannot be changed later.", "apps.appNameWarning": "The app name cannot be changed later.",
"apps.appsButtonCreate": "Apps Overview", "apps.appsButtonCreate": "Apps Overview",
"apps.appsButtonFallbackTitle": "Apps Overview", "apps.appsButtonFallbackTitle": "Apps Overview",
"apps.archive": "Archive App",
"apps.archiveConfirmText": "Do you really want to archive this app?",
"apps.archiveConfirmTitle": "Archive App",
"apps.archiveFailed": "Failed to archive app. Please reload.",
"apps.archiveWarning": "Once you archive an app, there is no going back. Please be certain.",
"apps.create": "Create App", "apps.create": "Create App",
"apps.createBlankApp": "New App", "apps.createBlankApp": "New App",
"apps.createBlankAppDescription": "Create a new blank app without content and schemas.", "apps.createBlankAppDescription": "Create a new blank app without content and schemas.",
@ -26,9 +21,14 @@
"apps.createProfileApp": "New Profile Sample", "apps.createProfileApp": "New Profile Sample",
"apps.createProfileAppDescription": "Create your profile page.", "apps.createProfileAppDescription": "Create your profile page.",
"apps.createWithTemplate": "Create {template} Sample", "apps.createWithTemplate": "Create {template} Sample",
"apps.delete": "Delete App",
"apps.deleteConfirmText": "Do you really want to delete this app?",
"apps.deleteConfirmTitle": "I understand. Delete my App",
"apps.deleteFailed": "Failed to delete app. Please reload.",
"apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.",
"apps.empty": "You are not collaborating on any apps yet", "apps.empty": "You are not collaborating on any apps yet",
"apps.generalSettings": "General", "apps.generalSettings": "General",
"apps.generalSettingsDangerZone": "General", "apps.generalSettingsDangerZone": "Danger Zone",
"apps.image": "Image", "apps.image": "Image",
"apps.imageDrop": "Drop to upload", "apps.imageDrop": "Drop to upload",
"apps.leave": "Leave app", "apps.leave": "Leave app",

10
backend/i18n/frontend_it.json

@ -12,11 +12,6 @@
"apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.", "apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.",
"apps.appsButtonCreate": "Nuova App", "apps.appsButtonCreate": "Nuova App",
"apps.appsButtonFallbackTitle": "Lista App", "apps.appsButtonFallbackTitle": "Lista App",
"apps.archive": "Archivia l'App",
"apps.archiveConfirmText": "Rimuovi il pattern",
"apps.archiveConfirmTitle": "Sei sicuro di voler archiviare questa app?",
"apps.archiveFailed": "Non è stato possibile archiviare l'app. Per favore ricarica.",
"apps.archiveWarning": "Una volta archiviata una App, non è possibile tornare indietro. Sii certo.",
"apps.create": "Crea un'App", "apps.create": "Crea un'App",
"apps.createBlankApp": "Nuova App.", "apps.createBlankApp": "Nuova App.",
"apps.createBlankAppDescription": "Crea una app vuota senza contenuti o schema.", "apps.createBlankAppDescription": "Crea una app vuota senza contenuti o schema.",
@ -26,6 +21,11 @@
"apps.createProfileApp": "Nuovo Profilo", "apps.createProfileApp": "Nuovo Profilo",
"apps.createProfileAppDescription": "Crea la tua pagina del profilo.", "apps.createProfileAppDescription": "Crea la tua pagina del profilo.",
"apps.createWithTemplate": "Create un esempio di {template}", "apps.createWithTemplate": "Create un esempio di {template}",
"apps.delete": "Delete App",
"apps.deleteConfirmText": "Do you really want to delete this app?",
"apps.deleteConfirmTitle": "I understand. Delete my App",
"apps.deleteFailed": "Failed to delete app. Please reload.",
"apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.",
"apps.empty": "Non stai ancora collaborando su nessuna app", "apps.empty": "Non stai ancora collaborando su nessuna app",
"apps.generalSettings": "Generale", "apps.generalSettings": "Generale",
"apps.generalSettingsDangerZone": "Generale", "apps.generalSettingsDangerZone": "Generale",

10
backend/i18n/frontend_nl.json

@ -12,11 +12,6 @@
"apps.appNameWarning": "De app-naam kan later niet worden gewijzigd.", "apps.appNameWarning": "De app-naam kan later niet worden gewijzigd.",
"apps.appsButtonCreate": "Apps-overzicht", "apps.appsButtonCreate": "Apps-overzicht",
"apps.appsButtonFallbackTitle": "Apps-overzicht", "apps.appsButtonFallbackTitle": "Apps-overzicht",
"apps.archive": "App archiveren",
"apps.archiveConfirmText": "Patroon verwijderen",
"apps.archiveConfirmTitle": "Wil je deze app echt archiveren?",
"apps.archiveFailed": "Kan app niet archiveren. Laad opnieuw.",
"apps.archiveWarning": "Zodra je een app archiveert, is er geen weg meer terug. Wees alsjeblieft zeker.",
"apps.create": "App maken", "apps.create": "App maken",
"apps.createBlankApp": "Nieuwe app.", "apps.createBlankApp": "Nieuwe app.",
"apps.createBlankAppDescription": "Maak een nieuwe lege app zonder inhoud en schema's.", "apps.createBlankAppDescription": "Maak een nieuwe lege app zonder inhoud en schema's.",
@ -26,6 +21,11 @@
"apps.createProfileApp": "Nieuw profielvoorbeeld", "apps.createProfileApp": "Nieuw profielvoorbeeld",
"apps.createProfileAppDescription": "Maak uw profielpagina.", "apps.createProfileAppDescription": "Maak uw profielpagina.",
"apps.createWithTemplate": "Maak {sjabloon} voorbeeld", "apps.createWithTemplate": "Maak {sjabloon} voorbeeld",
"apps.delete": "Delete App",
"apps.deleteConfirmText": "Do you really want to delete this app?",
"apps.deleteConfirmTitle": "I understand. Delete my App",
"apps.deleteFailed": "Failed to delete app. Please reload.",
"apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.",
"apps.empty": "Je werkt nog niet samen aan een app", "apps.empty": "Je werkt nog niet samen aan een app",
"apps.generalSettings": "Algemeen", "apps.generalSettings": "Algemeen",
"apps.generalSettingsDangerZone": "Algemeen", "apps.generalSettingsDangerZone": "Algemeen",

10
backend/i18n/frontend_zh.json

@ -12,11 +12,6 @@
"apps.appNameWarning": "以后不能更改应用名称。", "apps.appNameWarning": "以后不能更改应用名称。",
"apps.appsButtonCreate": "应用概览", "apps.appsButtonCreate": "应用概览",
"apps.appsButtonFallbackTitle": "应用概览", "apps.appsButtonFallbackTitle": "应用概览",
"apps.archive": "存档应用",
"apps.archiveConfirmText": "你真的要存档这个应用程序吗?",
"apps.archiveConfirmTitle": "存档应用程序",
"apps.archiveFailed": "存档应用失败。请重新加载。",
"apps.archiveWarning": "一旦你归档了一个应用程序,就没有回头路了。请确定。",
"apps.create": "创建应用程序", "apps.create": "创建应用程序",
"apps.createBlankApp": "新应用程序", "apps.createBlankApp": "新应用程序",
"apps.createBlankAppDescription": "创建一个没有内容和Schemas的新空白应用程序。", "apps.createBlankAppDescription": "创建一个没有内容和Schemas的新空白应用程序。",
@ -26,6 +21,11 @@
"apps.createProfileApp": "新配置文件示例", "apps.createProfileApp": "新配置文件示例",
"apps.createProfileAppDescription": "创建您的个人资料页面。", "apps.createProfileAppDescription": "创建您的个人资料页面。",
"apps.createWithTemplate": "创建 {template} 示例", "apps.createWithTemplate": "创建 {template} 示例",
"apps.delete": "Delete App",
"apps.deleteConfirmText": "Do you really want to delete this app?",
"apps.deleteConfirmTitle": "I understand. Delete my App",
"apps.deleteFailed": "Failed to delete app. Please reload.",
"apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.",
"apps.empty": "您还没有与任何应用协作", "apps.empty": "您还没有与任何应用协作",
"apps.generalSettings": "通用", "apps.generalSettings": "通用",
"apps.generalSettingsDangerZone": "通用", "apps.generalSettingsDangerZone": "通用",

12
backend/i18n/source/frontend_en.json

@ -12,11 +12,6 @@
"apps.appNameWarning": "The app name cannot be changed later.", "apps.appNameWarning": "The app name cannot be changed later.",
"apps.appsButtonCreate": "Apps Overview", "apps.appsButtonCreate": "Apps Overview",
"apps.appsButtonFallbackTitle": "Apps Overview", "apps.appsButtonFallbackTitle": "Apps Overview",
"apps.archive": "Archive App",
"apps.archiveConfirmText": "Do you really want to archive this app?",
"apps.archiveConfirmTitle": "Archive App",
"apps.archiveFailed": "Failed to archive app. Please reload.",
"apps.archiveWarning": "Once you archive an app, there is no going back. Please be certain.",
"apps.create": "Create App", "apps.create": "Create App",
"apps.createBlankApp": "New App", "apps.createBlankApp": "New App",
"apps.createBlankAppDescription": "Create a new blank app without content and schemas.", "apps.createBlankAppDescription": "Create a new blank app without content and schemas.",
@ -26,9 +21,14 @@
"apps.createProfileApp": "New Profile Sample", "apps.createProfileApp": "New Profile Sample",
"apps.createProfileAppDescription": "Create your profile page.", "apps.createProfileAppDescription": "Create your profile page.",
"apps.createWithTemplate": "Create {template} Sample", "apps.createWithTemplate": "Create {template} Sample",
"apps.delete": "Delete App",
"apps.deleteConfirmText": "Do you really want to delete this app?",
"apps.deleteConfirmTitle": "I understand. Delete my App",
"apps.deleteFailed": "Failed to delete app. Please reload.",
"apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.",
"apps.empty": "You are not collaborating on any apps yet", "apps.empty": "You are not collaborating on any apps yet",
"apps.generalSettings": "General", "apps.generalSettings": "General",
"apps.generalSettingsDangerZone": "General", "apps.generalSettingsDangerZone": "Danger Zone",
"apps.image": "Image", "apps.image": "Image",
"apps.imageDrop": "Drop to upload", "apps.imageDrop": "Drop to upload",
"apps.leave": "Leave app", "apps.leave": "Leave app",

5
backend/i18n/source/frontend_it.json

@ -12,11 +12,6 @@
"apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.", "apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.",
"apps.appsButtonCreate": "Nuova App", "apps.appsButtonCreate": "Nuova App",
"apps.appsButtonFallbackTitle": "Lista App", "apps.appsButtonFallbackTitle": "Lista App",
"apps.archive": "Archivia l'App",
"apps.archiveConfirmText": "Rimuovi il pattern",
"apps.archiveConfirmTitle": "Sei sicuro di voler archiviare questa app?",
"apps.archiveFailed": "Non è stato possibile archiviare l'app. Per favore ricarica.",
"apps.archiveWarning": "Una volta archiviata una App, non è possibile tornare indietro. Sii certo.",
"apps.create": "Crea un'App", "apps.create": "Crea un'App",
"apps.createBlankApp": "Nuova App.", "apps.createBlankApp": "Nuova App.",
"apps.createBlankAppDescription": "Crea una app vuota senza contenuti o schema.", "apps.createBlankAppDescription": "Crea una app vuota senza contenuti o schema.",

5
backend/i18n/source/frontend_nl.json

@ -12,11 +12,6 @@
"apps.appNameWarning": "De app-naam kan later niet worden gewijzigd.", "apps.appNameWarning": "De app-naam kan later niet worden gewijzigd.",
"apps.appsButtonCreate": "Apps-overzicht", "apps.appsButtonCreate": "Apps-overzicht",
"apps.appsButtonFallbackTitle": "Apps-overzicht", "apps.appsButtonFallbackTitle": "Apps-overzicht",
"apps.archive": "App archiveren",
"apps.archiveConfirmText": "Patroon verwijderen",
"apps.archiveConfirmTitle": "Wil je deze app echt archiveren?",
"apps.archiveFailed": "Kan app niet archiveren. Laad opnieuw.",
"apps.archiveWarning": "Zodra je een app archiveert, is er geen weg meer terug. Wees alsjeblieft zeker.",
"apps.create": "App maken", "apps.create": "App maken",
"apps.createBlankApp": "Nieuwe app.", "apps.createBlankApp": "Nieuwe app.",
"apps.createBlankAppDescription": "Maak een nieuwe lege app zonder inhoud en schema's.", "apps.createBlankAppDescription": "Maak een nieuwe lege app zonder inhoud en schema's.",

5
backend/i18n/source/frontend_zh.json

@ -12,11 +12,6 @@
"apps.appNameWarning": "以后不能更改应用名称。", "apps.appNameWarning": "以后不能更改应用名称。",
"apps.appsButtonCreate": "应用概览", "apps.appsButtonCreate": "应用概览",
"apps.appsButtonFallbackTitle": "应用概览", "apps.appsButtonFallbackTitle": "应用概览",
"apps.archive": "存档应用",
"apps.archiveConfirmText": "你真的要存档这个应用程序吗?",
"apps.archiveConfirmTitle": "存档应用程序",
"apps.archiveFailed": "存档应用失败。请重新加载。",
"apps.archiveWarning": "一旦你归档了一个应用程序,就没有回头路了。请确定。",
"apps.create": "创建应用程序", "apps.create": "创建应用程序",
"apps.createBlankApp": "新应用程序", "apps.createBlankApp": "新应用程序",
"apps.createBlankAppDescription": "创建一个没有内容和Schemas的新空白应用程序。", "apps.createBlankAppDescription": "创建一个没有内容和Schemas的新空白应用程序。",

23
backend/src/Migrations/MigrationPath.cs

@ -18,7 +18,7 @@ namespace Migrations
{ {
public sealed class MigrationPath : IMigrationPath public sealed class MigrationPath : IMigrationPath
{ {
private const int CurrentVersion = 25; private const int CurrentVersion = 26;
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public MigrationPath(IServiceProvider serviceProvider) public MigrationPath(IServiceProvider serviceProvider)
@ -75,17 +75,12 @@ namespace Migrations
// Version 12: Introduce roles. // Version 12: Introduce roles.
// Version 24: Improve a naming in the languages config. // Version 24: Improve a naming in the languages config.
if (version < 24) // Version 26: Introduce full deletion.
if (version < 26)
{ {
yield return serviceProvider.GetRequiredService<RebuildApps>(); // yield return serviceProvider.GetRequiredService<RebuildApps>();
} // yield return serviceProvider.GetRequiredService<RebuildSchemas>();
yield return serviceProvider.GetRequiredService<RebuildRules>();
// Version 14: Schema refactoring
// Version 22: Introduce domain id.
if (version < 22)
{
yield return serviceProvider.GetRequiredService<ClearSchemas>();
yield return serviceProvider.GetRequiredService<ClearRules>();
} }
// Version 18: Rebuild assets. // Version 18: Rebuild assets.
@ -130,12 +125,6 @@ namespace Migrations
yield return serviceProvider.GetRequiredService<ConvertRuleEventsJson>(); yield return serviceProvider.GetRequiredService<ConvertRuleEventsJson>();
} }
// Version 19: Unify indexes.
if (version < 19)
{
yield return serviceProvider.GetRequiredService<PopulateGrainIndexes>();
}
yield return serviceProvider.GetRequiredService<StartEventConsumers>(); yield return serviceProvider.GetRequiredService<StartEventConsumers>();
} }
} }

4
backend/src/Migrations/Migrations.csproj

@ -5,6 +5,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.670">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup> </ItemGroup>

3
backend/src/Migrations/Migrations/ClearRules.cs

@ -22,7 +22,8 @@ namespace Migrations.Migrations
this.store = store; this.store = store;
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
return store.ClearSnapshotsAsync(); return store.ClearSnapshotsAsync();
} }

3
backend/src/Migrations/Migrations/ClearSchemas.cs

@ -22,7 +22,8 @@ namespace Migrations.Migrations
this.store = store; this.store = store;
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
return store.ClearSnapshotsAsync(); return store.ClearSnapshotsAsync();
} }

3
backend/src/Migrations/Migrations/ConvertEventStore.cs

@ -24,7 +24,8 @@ namespace Migrations.Migrations
this.eventStore = eventStore; this.eventStore = eventStore;
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
if (eventStore is MongoEventStore mongoEventStore) if (eventStore is MongoEventStore mongoEventStore)
{ {

3
backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs

@ -26,7 +26,8 @@ namespace Migrations.Migrations
this.eventStore = eventStore; this.eventStore = eventStore;
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
if (eventStore is MongoEventStore mongoEventStore) if (eventStore is MongoEventStore mongoEventStore)
{ {

9
backend/src/Migrations/Migrations/CreateAssetSlugs.cs

@ -24,16 +24,17 @@ namespace Migrations.Migrations
this.stateForAssets = stateForAssets; this.stateForAssets = stateForAssets;
} }
public Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
return stateForAssets.ReadAllAsync(async (state, version) => await foreach (var (state, version) in stateForAssets.ReadAllAsync(ct))
{ {
state.Slug = state.FileName.ToAssetSlug(); state.Slug = state.FileName.ToAssetSlug();
var key = DomainId.Combine(state.AppId.Id, state.Id); var key = DomainId.Combine(state.AppId.Id, state.Id);
await stateForAssets.WriteAsync(key, state, version, version); await stateForAssets.WriteAsync(key, state, version, version, ct);
}, ct); }
} }
} }
} }

20
backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs

@ -15,6 +15,7 @@ using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Migrations.Migrations.MongoDb namespace Migrations.Migrations.MongoDb
@ -28,13 +29,14 @@ namespace Migrations.Migrations.MongoDb
this.database = database; this.database = database;
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
const int SizeOfBatch = 1000; const int SizeOfBatch = 1000;
const int SizeOfQueue = 20; const int SizeOfQueue = 20;
var collectionOld = database.GetCollection<BsonDocument>("Events"); var collectionV1 = database.GetCollection<BsonDocument>("Events");
var collectionNew = database.GetCollection<BsonDocument>("Events2"); var collectionV2 = database.GetCollection<BsonDocument>("Events2");
var batchBlock = new BatchBlock<BsonDocument>(SizeOfBatch, new GroupingDataflowBlockOptions var batchBlock = new BatchBlock<BsonDocument>(SizeOfBatch, new GroupingDataflowBlockOptions
{ {
@ -60,7 +62,7 @@ namespace Migrations.Migrations.MongoDb
{ {
if (!eventStream.StartsWith("app-", StringComparison.OrdinalIgnoreCase)) if (!eventStream.StartsWith("app-", StringComparison.OrdinalIgnoreCase))
{ {
var indexOfType = eventStream.IndexOf('-'); var indexOfType = eventStream.IndexOf('-', StringComparison.Ordinal);
var indexOfId = indexOfType + 1; var indexOfId = indexOfType + 1;
var indexOfOldId = eventStream.LastIndexOf("--", StringComparison.OrdinalIgnoreCase); var indexOfOldId = eventStream.LastIndexOf("--", StringComparison.OrdinalIgnoreCase);
@ -104,7 +106,7 @@ namespace Migrations.Migrations.MongoDb
if (writes.Count > 0) if (writes.Count > 0)
{ {
await collectionNew.BulkWriteAsync(writes, writeOptions); await collectionV2.BulkWriteAsync(writes, writeOptions, ct);
} }
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
@ -121,7 +123,13 @@ namespace Migrations.Migrations.MongoDb
batchBlock.BidirectionalLinkTo(actionBlock); batchBlock.BidirectionalLinkTo(actionBlock);
await collectionOld.Find(new BsonDocument()).ForEachAsync(batchBlock.SendAsync, ct); await foreach (var commit in collectionV1.Find(new BsonDocument()).ToAsyncEnumerable(ct: ct))
{
if (!await batchBlock.SendAsync(commit, ct))
{
break;
}
}
batchBlock.Complete(); batchBlock.Complete();

30
backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs

@ -57,7 +57,8 @@ namespace Migrations.Migrations.MongoDb
return this; return this;
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
switch (scope) switch (scope)
{ {
@ -72,25 +73,26 @@ namespace Migrations.Migrations.MongoDb
} }
} }
private static async Task RebuildAsync(IMongoDatabase database, Action<BsonDocument>? extraAction, string collectionNameOld, CancellationToken ct) private static async Task RebuildAsync(IMongoDatabase database, Action<BsonDocument>? extraAction, string collectionNameV1,
CancellationToken ct)
{ {
const int SizeOfBatch = 1000; const int SizeOfBatch = 1000;
const int SizeOfQueue = 10; const int SizeOfQueue = 10;
string collectionNameNew; string collectionNameV2;
collectionNameNew = $"{collectionNameOld}2"; collectionNameV2 = $"{collectionNameV1}2";
collectionNameNew = collectionNameNew.Replace("State_", "States_"); collectionNameV2 = collectionNameV2.Replace("State_", "States_", StringComparison.Ordinal);
var collectionOld = database.GetCollection<BsonDocument>(collectionNameOld); var collectionV1 = database.GetCollection<BsonDocument>(collectionNameV1);
var collectionNew = database.GetCollection<BsonDocument>(collectionNameNew); var collectionV2 = database.GetCollection<BsonDocument>(collectionNameV2);
if (!await collectionOld.AnyAsync()) if (!await collectionV1.AnyAsync(ct: ct))
{ {
return; return;
} }
await collectionNew.DeleteManyAsync(new BsonDocument(), ct); await collectionV2.DeleteManyAsync(new BsonDocument(), ct);
var batchBlock = new BatchBlock<BsonDocument>(SizeOfBatch, new GroupingDataflowBlockOptions var batchBlock = new BatchBlock<BsonDocument>(SizeOfBatch, new GroupingDataflowBlockOptions
{ {
@ -138,7 +140,7 @@ namespace Migrations.Migrations.MongoDb
if (writes.Count > 0) if (writes.Count > 0)
{ {
await collectionNew.BulkWriteAsync(writes, writeOptions); await collectionV2.BulkWriteAsync(writes, writeOptions, ct);
} }
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
@ -155,7 +157,13 @@ namespace Migrations.Migrations.MongoDb
batchBlock.BidirectionalLinkTo(actionBlock); batchBlock.BidirectionalLinkTo(actionBlock);
await collectionOld.Find(new BsonDocument()).ForEachAsync(batchBlock.SendAsync, ct); await foreach (var document in collectionV1.Find(new BsonDocument()).ToAsyncEnumerable(ct: ct))
{
if (!await batchBlock.SendAsync(document, ct))
{
break;
}
}
batchBlock.Complete(); batchBlock.Complete();

5
backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs

@ -23,7 +23,8 @@ namespace Migrations.Migrations.MongoDb
this.database = database; this.database = database;
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
var collections = new[] var collections = new[]
{ {
@ -39,7 +40,7 @@ namespace Migrations.Migrations.MongoDb
return Task.WhenAll( return Task.WhenAll(
collections collections
.Select(x => database.GetCollection<BsonDocument>(x)) .Select(x => database.GetCollection<BsonDocument>(x))
.Select(x => x.UpdateManyAsync(filter, update))); .Select(x => x.UpdateManyAsync(filter, update, cancellationToken: ct)));
} }
} }
} }

3
backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs

@ -22,7 +22,8 @@ namespace Migrations.Migrations.MongoDb
collection = database.GetCollection<BsonDocument>("RuleEvents"); collection = database.GetCollection<BsonDocument>("RuleEvents");
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
foreach (var document in collection.Find(new BsonDocument()).ToEnumerable(ct)) foreach (var document in collection.Find(new BsonDocument()).ToEnumerable(ct))
{ {

3
backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs

@ -21,7 +21,8 @@ namespace Migrations.Migrations.MongoDb
this.database = database; this.database = database;
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
await database.DropCollectionAsync("States_Contents", ct); await database.DropCollectionAsync("States_Contents", ct);
await database.DropCollectionAsync("States_Contents_Archive", ct); await database.DropCollectionAsync("States_Contents_Archive", ct);

3
backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs

@ -22,7 +22,8 @@ namespace Migrations.Migrations.MongoDb
this.database = database; this.database = database;
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
var collection = database.GetCollection<BsonDocument>("States_Assets"); var collection = database.GetCollection<BsonDocument>("States_Assets");

3
backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs

@ -22,7 +22,8 @@ namespace Migrations.Migrations.MongoDb
this.database = database; this.database = database;
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
var collection = database.GetCollection<BsonDocument>("States_Assets"); var collection = database.GetCollection<BsonDocument>("States_Assets");

7
backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs

@ -23,16 +23,17 @@ namespace Migrations.Migrations.MongoDb
this.contentDatabase = contentDatabase; this.contentDatabase = contentDatabase;
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
if (await contentDatabase.CollectionExistsAsync("State_Content_Draft")) if (await contentDatabase.CollectionExistsAsync("State_Content_Draft", ct))
{ {
await contentDatabase.DropCollectionAsync("State_Contents", ct); await contentDatabase.DropCollectionAsync("State_Contents", ct);
await contentDatabase.DropCollectionAsync("State_Content_Published", ct); await contentDatabase.DropCollectionAsync("State_Content_Published", ct);
await contentDatabase.RenameCollectionAsync("State_Content_Draft", "State_Contents", cancellationToken: ct); await contentDatabase.RenameCollectionAsync("State_Content_Draft", "State_Contents", cancellationToken: ct);
} }
if (await contentDatabase.CollectionExistsAsync("State_Contents")) if (await contentDatabase.CollectionExistsAsync("State_Contents", ct))
{ {
var collection = contentDatabase.GetCollection<BsonDocument>("State_Contents"); var collection = contentDatabase.GetCollection<BsonDocument>("State_Contents");

189
backend/src/Migrations/Migrations/PopulateGrainIndexes.cs

@ -1,189 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Rules.Indexes;
using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations;
namespace Migrations.Migrations
{
public class PopulateGrainIndexes : IMigration
{
private readonly IAppsIndex indexApps;
private readonly IRulesIndex indexRules;
private readonly ISchemasIndex indexSchemas;
private readonly IEventDataFormatter eventDataFormatter;
private readonly IEventStore eventStore;
public PopulateGrainIndexes(IAppsIndex indexApps, IRulesIndex indexRules, ISchemasIndex indexSchemas,
IEventDataFormatter eventDataFormatter,
IEventStore eventStore)
{
this.indexApps = indexApps;
this.indexRules = indexRules;
this.indexSchemas = indexSchemas;
this.eventDataFormatter = eventDataFormatter;
this.eventStore = eventStore;
}
public Task UpdateAsync(CancellationToken ct)
{
return Task.WhenAll(
RebuildAppIndexes(ct),
RebuildRuleIndexes(ct),
RebuildSchemaIndexes(ct));
}
private async Task RebuildAppIndexes(CancellationToken ct)
{
var appsByName = new Dictionary<string, DomainId>();
var appsByUser = new Dictionary<string, HashSet<DomainId>>();
bool HasApp(NamedId<DomainId> appId, bool consistent, out DomainId id)
{
return appsByName!.TryGetValue(appId.Name, out id) && (!consistent || id == appId.Id);
}
HashSet<DomainId> Index(string contributorId)
{
return appsByUser!.GetOrAddNew(contributorId);
}
void RemoveApp(NamedId<DomainId> appId, bool consistent)
{
if (HasApp(appId, consistent, out var id))
{
foreach (var apps in appsByUser!.Values)
{
apps.Remove(id);
}
appsByName!.Remove(appId.Name);
}
}
await foreach (var storedEvent in eventStore.QueryAllAsync("^app\\-", ct: ct))
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
if (@event != null)
{
switch (@event.Payload)
{
case AppCreated created:
{
RemoveApp(created.AppId, false);
appsByName[created.Name] = created.AppId.Id;
break;
}
case AppContributorAssigned contributorAssigned:
{
if (HasApp(contributorAssigned.AppId, true, out _))
{
Index(contributorAssigned.ContributorId).Add(contributorAssigned.AppId.Id);
}
break;
}
case AppContributorRemoved contributorRemoved:
Index(contributorRemoved.ContributorId).Remove(contributorRemoved.AppId.Id);
break;
case AppArchived archived:
RemoveApp(archived.AppId, true);
break;
}
}
}
await indexApps.RebuildAsync(appsByName);
foreach (var (contributorId, apps) in appsByUser)
{
await indexApps.RebuildByContributorsAsync(contributorId, apps);
}
}
private async Task RebuildRuleIndexes(CancellationToken ct)
{
var rulesByApp = new Dictionary<DomainId, HashSet<DomainId>>();
HashSet<DomainId> Index(RuleEvent @event)
{
return rulesByApp!.GetOrAddNew(@event.AppId.Id);
}
await foreach (var storedEvent in eventStore.QueryAllAsync("^rule\\-", ct: ct))
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
if (@event != null)
{
switch (@event.Payload)
{
case RuleCreated created:
Index(created).Add(created.RuleId);
break;
case RuleDeleted deleted:
Index(deleted).Remove(deleted.RuleId);
break;
}
}
}
foreach (var (appId, rules) in rulesByApp)
{
await indexRules.RebuildAsync(appId, rules);
}
}
private async Task RebuildSchemaIndexes(CancellationToken ct)
{
var schemasByApp = new Dictionary<DomainId, Dictionary<string, DomainId>>();
Dictionary<string, DomainId> Index(SchemaEvent @event)
{
return schemasByApp!.GetOrAddNew(@event.AppId.Id);
}
await foreach (var storedEvent in eventStore.QueryAllAsync("^schema\\-", ct: ct))
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
if (@event != null)
{
switch (@event.Payload)
{
case SchemaCreated created:
Index(created)[created.SchemaId.Name] = created.SchemaId.Id;
break;
case SchemaDeleted deleted:
Index(deleted).Remove(deleted.SchemaId.Name);
break;
}
}
}
foreach (var (appId, schemas) in schemasByApp)
{
await indexSchemas.RebuildAsync(appId, schemas);
}
}
}
}

3
backend/src/Migrations/Migrations/RebuildApps.cs

@ -25,7 +25,8 @@ namespace Migrations.Migrations
this.rebuildOptions = rebuildOptions.Value; this.rebuildOptions = rebuildOptions.Value;
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
return rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); return rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct);
} }

3
backend/src/Migrations/Migrations/RebuildAssetFolders.cs

@ -25,7 +25,8 @@ namespace Migrations.Migrations
this.rebuildOptions = rebuildOptions.Value; this.rebuildOptions = rebuildOptions.Value;
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
return rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct); return rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct);
} }

3
backend/src/Migrations/Migrations/RebuildAssets.cs

@ -25,7 +25,8 @@ namespace Migrations.Migrations
this.rebuildOptions = rebuildOptions.Value; this.rebuildOptions = rebuildOptions.Value;
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
return rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct); return rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct);
} }

3
backend/src/Migrations/Migrations/RebuildContents.cs

@ -25,7 +25,8 @@ namespace Migrations.Migrations
this.rebuildOptions = rebuildOptions.Value; this.rebuildOptions = rebuildOptions.Value;
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
return rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct); return rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct);
} }

34
backend/src/Migrations/Migrations/RebuildRules.cs

@ -0,0 +1,34 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Migrations;
namespace Migrations.Migrations
{
public sealed class RebuildRules : IMigration
{
private readonly Rebuilder rebuilder;
private readonly RebuildOptions rebuildOptions;
public RebuildRules(Rebuilder rebuilder,
IOptions<RebuildOptions> rebuildOptions)
{
this.rebuilder = rebuilder;
this.rebuildOptions = rebuildOptions.Value;
}
public Task UpdateAsync(
CancellationToken ct)
{
return rebuilder.RebuildRulesAsync(rebuildOptions.BatchSize, ct);
}
}
}

34
backend/src/Migrations/Migrations/RebuildSchemas.cs

@ -0,0 +1,34 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Migrations;
namespace Migrations.Migrations
{
public sealed class RebuildSchemas : IMigration
{
private readonly Rebuilder rebuilder;
private readonly RebuildOptions rebuildOptions;
public RebuildSchemas(Rebuilder rebuilder,
IOptions<RebuildOptions> rebuildOptions)
{
this.rebuilder = rebuilder;
this.rebuildOptions = rebuildOptions.Value;
}
public Task UpdateAsync(
CancellationToken ct)
{
return rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct);
}
}
}

3
backend/src/Migrations/Migrations/RebuildSnapshots.cs

@ -25,7 +25,8 @@ namespace Migrations.Migrations
this.rebuildOptions = rebuildOptions.Value; this.rebuildOptions = rebuildOptions.Value;
} }
public async Task UpdateAsync(CancellationToken ct) public async Task UpdateAsync(
CancellationToken ct)
{ {
await rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); await rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct);
await rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); await rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct);

3
backend/src/Migrations/Migrations/StartEventConsumers.cs

@ -23,7 +23,8 @@ namespace Migrations.Migrations
eventConsumerManager = grainFactory.GetGrain<IEventConsumerManagerGrain>(SingleGrain.Id); eventConsumerManager = grainFactory.GetGrain<IEventConsumerManagerGrain>(SingleGrain.Id);
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
return eventConsumerManager.StartAllAsync(); return eventConsumerManager.StartAllAsync();
} }

3
backend/src/Migrations/Migrations/StopEventConsumers.cs

@ -23,7 +23,8 @@ namespace Migrations.Migrations
eventConsumerManager = grainFactory.GetGrain<IEventConsumerManagerGrain>(SingleGrain.Id); eventConsumerManager = grainFactory.GetGrain<IEventConsumerManagerGrain>(SingleGrain.Id);
} }
public Task UpdateAsync(CancellationToken ct) public Task UpdateAsync(
CancellationToken ct)
{ {
return eventConsumerManager.StopAllAsync(); return eventConsumerManager.StopAllAsync();
} }

24
backend/src/Migrations/OldEvents/AppArchived.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Reflection;
namespace Migrations.OldEvents
{
[EventType(nameof(AppArchived))]
public sealed class AppArchived : AppEvent, IMigrated<IEvent>
{
public IEvent Migrate()
{
return SimpleMapper.Map(this, new AppDeleted());
}
}
}

5
backend/src/Migrations/OldEvents/AppClientChanged.cs

@ -23,7 +23,10 @@ namespace Migrations.OldEvents
public IEvent Migrate() public IEvent Migrate()
{ {
var permission = IsReader ? AppClientPermission.Reader : AppClientPermission.Editor; var permission =
IsReader ?
AppClientPermission.Reader :
AppClientPermission.Editor;
return SimpleMapper.Map(this, new AppClientUpdated { Permission = permission }); return SimpleMapper.Map(this, new AppClientUpdated { Permission = permission });
} }

14
backend/src/Migrations/RebuildRunner.cs

@ -8,7 +8,6 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Migrations.Migrations;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -18,22 +17,20 @@ namespace Migrations
{ {
private readonly RebuildFiles rebuildFiles; private readonly RebuildFiles rebuildFiles;
private readonly Rebuilder rebuilder; private readonly Rebuilder rebuilder;
private readonly PopulateGrainIndexes populateGrainIndexes;
private readonly RebuildOptions rebuildOptions; private readonly RebuildOptions rebuildOptions;
public RebuildRunner( public RebuildRunner(
IOptions<RebuildOptions> rebuildOptions, IOptions<RebuildOptions> rebuildOptions,
Rebuilder rebuilder, Rebuilder rebuilder,
RebuildFiles rebuildFiles, RebuildFiles rebuildFiles)
PopulateGrainIndexes populateGrainIndexes)
{ {
this.rebuildFiles = rebuildFiles; this.rebuildFiles = rebuildFiles;
this.rebuilder = rebuilder; this.rebuilder = rebuilder;
this.rebuildOptions = rebuildOptions.Value; this.rebuildOptions = rebuildOptions.Value;
this.populateGrainIndexes = populateGrainIndexes;
} }
public async Task RunAsync(CancellationToken ct) public async Task RunAsync(
CancellationToken ct)
{ {
var batchSize = rebuildOptions.CalculateBatchSize(); var batchSize = rebuildOptions.CalculateBatchSize();
@ -67,11 +64,6 @@ namespace Migrations
{ {
await rebuilder.RebuildContentAsync(batchSize, ct); await rebuilder.RebuildContentAsync(batchSize, ct);
} }
if (rebuildOptions.Indexes)
{
await populateGrainIndexes.UpdateAsync(ct);
}
} }
} }
} }

18
backend/src/Migrations/RebuilderExtensions.cs

@ -18,32 +18,38 @@ namespace Migrations
{ {
public static class RebuilderExtensions public static class RebuilderExtensions
{ {
public static Task RebuildAppsAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) public static Task RebuildAppsAsync(this Rebuilder rebuilder, int batchSize,
CancellationToken ct = default)
{ {
return rebuilder.RebuildAsync<AppDomainObject, AppDomainObject.State>("^app\\-", batchSize, ct); return rebuilder.RebuildAsync<AppDomainObject, AppDomainObject.State>("^app\\-", batchSize, ct);
} }
public static Task RebuildSchemasAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) public static Task RebuildSchemasAsync(this Rebuilder rebuilder, int batchSize,
CancellationToken ct = default)
{ {
return rebuilder.RebuildAsync<SchemaDomainObject, SchemaDomainObject.State>("^schema\\-", batchSize, ct); return rebuilder.RebuildAsync<SchemaDomainObject, SchemaDomainObject.State>("^schema\\-", batchSize, ct);
} }
public static Task RebuildRulesAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) public static Task RebuildRulesAsync(this Rebuilder rebuilder, int batchSize,
CancellationToken ct = default)
{ {
return rebuilder.RebuildAsync<RuleDomainObject, RuleDomainObject.State>("^rule\\-", batchSize, ct); return rebuilder.RebuildAsync<RuleDomainObject, RuleDomainObject.State>("^rule\\-", batchSize, ct);
} }
public static Task RebuildAssetsAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) public static Task RebuildAssetsAsync(this Rebuilder rebuilder, int batchSize,
CancellationToken ct = default)
{ {
return rebuilder.RebuildAsync<AssetDomainObject, AssetDomainObject.State>("^asset\\-", batchSize, ct); return rebuilder.RebuildAsync<AssetDomainObject, AssetDomainObject.State>("^asset\\-", batchSize, ct);
} }
public static Task RebuildAssetFoldersAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) public static Task RebuildAssetFoldersAsync(this Rebuilder rebuilder, int batchSize,
CancellationToken ct = default)
{ {
return rebuilder.RebuildAsync<AssetFolderDomainObject, AssetFolderDomainObject.State>("^assetFolder\\-", batchSize, ct); return rebuilder.RebuildAsync<AssetFolderDomainObject, AssetFolderDomainObject.State>("^assetFolder\\-", batchSize, ct);
} }
public static Task RebuildContentAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) public static Task RebuildContentAsync(this Rebuilder rebuilder, int batchSize,
CancellationToken ct = default)
{ {
return rebuilder.RebuildAsync<ContentDomainObject, ContentDomainObject.State>("^content\\-", batchSize, ct); return rebuilder.RebuildAsync<ContentDomainObject, ContentDomainObject.State>("^content\\-", batchSize, ct);
} }

2
backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Contents
stream.Position = 0; stream.Position = 0;
geoJSON = serializer.Deserialize<GeoJSONObject>(stream, null, leaveOpen: true); geoJSON = serializer.Deserialize<GeoJSONObject>(stream, null, true);
return GeoJsonParseResult.Success; return GeoJsonParseResult.Success;
} }

4
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs

@ -36,12 +36,12 @@ namespace Squidex.Domain.Apps.Core.Contents
public bool Equals(Status other) public bool Equals(Status other)
{ {
return string.Equals(Name, other.Name); return string.Equals(Name, other.Name, StringComparison.Ordinal);
} }
public override int GetHashCode() public override int GetHashCode()
{ {
return Name.GetHashCode(); return Name.GetHashCode(StringComparison.Ordinal);
} }
public override string ToString() public override string ToString()

2
backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs

@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core
public override int GetHashCode() public override int GetHashCode()
{ {
return Key.GetHashCode(); return Key.GetHashCode(StringComparison.Ordinal);
} }
public override string ToString() public override string ToString()

2
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
[IgnoreDataMember] [IgnoreDataMember]
public override long Partition public override long Partition
{ {
get => MentionedUser?.Id.GetHashCode() ?? 0; get => MentionedUser?.Id.GetHashCode(StringComparison.Ordinal) ?? 0;
} }
public bool ShouldSerializeMentionedUser() public bool ShouldSerializeMentionedUser()

2
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Rules
[Pure] [Pure]
public Rule Rename(string newName) public Rule Rename(string newName)
{ {
if (string.Equals(Name, newName)) if (string.Equals(Name, newName, StringComparison.Ordinal))
{ {
return this; return this;
} }

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs

@ -206,7 +206,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public Schema ChangeCategory(string? category) public Schema ChangeCategory(string? category)
{ {
if (string.Equals(Category, category)) if (string.Equals(Category, category, StringComparison.Ordinal))
{ {
return this; return this;
} }

4
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -9,6 +9,10 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="1.0.670">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="5.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

5
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Microsoft.OData.Edm; using Microsoft.OData.Edm;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -18,12 +19,12 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{ {
public static string EscapeEdmField(this string field) public static string EscapeEdmField(this string field)
{ {
return field.Replace("-", "_"); return field.Replace("-", "_", StringComparison.Ordinal);
} }
public static string UnescapeEdmField(this string field) public static string UnescapeEdmField(this string field)
{ {
return field.Replace("_", "-"); return field.Replace("_", "-", StringComparison.Ordinal);
} }
public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory, public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory,

3
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs

@ -21,6 +21,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
Task<(string Description, object Data)> CreateJobAsync(EnrichedEvent @event, RuleAction action); Task<(string Description, object Data)> CreateJobAsync(EnrichedEvent @event, RuleAction action);
Task<Result> ExecuteJobAsync(object data, CancellationToken ct = default); Task<Result> ExecuteJobAsync(object data,
CancellationToken ct = default);
} }
} }

9
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs

@ -20,10 +20,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules
string GetName(AppEvent @event); string GetName(AppEvent @event);
IAsyncEnumerable<JobResult> CreateSnapshotJobsAsync(RuleContext context, CancellationToken ct = default); IAsyncEnumerable<JobResult> CreateSnapshotJobsAsync(RuleContext context,
CancellationToken ct = default);
IAsyncEnumerable<JobResult> CreateJobsAsync(Envelope<IEvent> @event, RuleContext context, CancellationToken ct = default); IAsyncEnumerable<JobResult> CreateJobsAsync(Envelope<IEvent> @event, RuleContext context,
CancellationToken ct = default);
Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job); Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job,
CancellationToken ct = default);
} }
} }

6
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs

@ -24,12 +24,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules
get => false; get => false;
} }
IAsyncEnumerable<EnrichedEvent> CreateSnapshotEventsAsync(RuleContext context, CancellationToken ct) IAsyncEnumerable<EnrichedEvent> CreateSnapshotEventsAsync(RuleContext context,
CancellationToken ct)
{ {
return AsyncEnumerable.Empty<EnrichedEvent>(); return AsyncEnumerable.Empty<EnrichedEvent>();
} }
IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context, CancellationToken ct); IAsyncEnumerable<EnrichedEvent> CreateEnrichedEventsAsync(Envelope<AppEvent> @event, RuleContext context,
CancellationToken ct);
string? GetName(AppEvent @event) string? GetName(AppEvent @event)
{ {

3
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Globalization;
using System.Text; using System.Text;
namespace Squidex.Domain.Apps.Core.HandleRules namespace Squidex.Domain.Apps.Core.HandleRules
@ -41,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
} }
dumpBuilder.AppendLine(); dumpBuilder.AppendLine();
dumpBuilder.AppendFormat("Elapsed {0}.", elapsed); dumpBuilder.AppendFormat(CultureInfo.InvariantCulture, "Elapsed {0}.", elapsed);
dumpBuilder.AppendLine(); dumpBuilder.AppendLine();
Dump = dumpBuilder.ToString(); Dump = dumpBuilder.ToString();

8
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs

@ -61,7 +61,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return (description, data!); return (description, data!);
} }
async Task<Result> IRuleActionHandler.ExecuteJobAsync(object data, CancellationToken ct) async Task<Result> IRuleActionHandler.ExecuteJobAsync(object data,
CancellationToken ct)
{ {
var typedData = (TData)data; var typedData = (TData)data;
@ -70,7 +71,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules
protected virtual Task<(string Description, TData Data)> CreateJobAsync(EnrichedEvent @event, TAction action) protected virtual Task<(string Description, TData Data)> CreateJobAsync(EnrichedEvent @event, TAction action)
{ {
#pragma warning disable MA0042 // Do not use blocking calls in an async method
return Task.FromResult(CreateJob(@event, action)); return Task.FromResult(CreateJob(@event, action));
#pragma warning restore MA0042 // Do not use blocking calls in an async method
} }
protected virtual (string Description, TData Data) CreateJob(EnrichedEvent @event, TAction action) protected virtual (string Description, TData Data) CreateJob(EnrichedEvent @event, TAction action)
@ -78,6 +81,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
throw new NotImplementedException(); throw new NotImplementedException();
} }
protected abstract Task<Result> ExecuteJobAsync(TData job, CancellationToken ct = default); protected abstract Task<Result> ExecuteJobAsync(TData job,
CancellationToken ct = default);
} }
} }

11
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -7,6 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -25,8 +26,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
public class RuleEventFormatter public class RuleEventFormatter
{ {
private const string GlobalFallback = "null"; private const string GlobalFallback = "null";
private static readonly Regex RegexPatternOld = new Regex(@"^(?<FullPath>(?<Type>[^_]*)_(?<Path>[^\s]*))", RegexOptions.Compiled); private static readonly Regex RegexPatternOld = new Regex(@"^(?<FullPath>(?<Type>[^_]*)_(?<Path>[^\s]*))", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
private static readonly Regex RegexPatternNew = new Regex(@"^\{(?<FullPath>(?<Type>[\w]+)_(?<Path>[\w\.\-]+))[\s]*(\|[\s]*(?<Transform>[^\?}]+))?(\?[\s]*(?<Fallback>[^\}\s]+))?[\s]*\}", RegexOptions.Compiled); private static readonly Regex RegexPatternNew = new Regex(@"^\{(?<FullPath>(?<Type>[\w]+)_(?<Path>[\w\.\-]+))[\s]*(\|[\s]*(?<Transform>[^\?}]+))?(\?[\s]*(?<Fallback>[^\}\s]+))?[\s]*\}", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly IEnumerable<IRuleEventFormatter> formatters; private readonly IEnumerable<IRuleEventFormatter> formatters;
private readonly ITemplateEngine templateEngine; private readonly ITemplateEngine templateEngine;
@ -111,7 +112,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules
["event"] = @event ["event"] = @event
}; };
#pragma warning disable MA0042 // Do not use blocking calls in an async method
var result = scriptEngine.Execute(vars, script).ToString(); var result = scriptEngine.Execute(vars, script).ToString();
#pragma warning restore MA0042 // Do not use blocking calls in an async method
if (result == "undefined") if (result == "undefined")
{ {
@ -285,7 +288,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
if (instant.Success) if (instant.Success)
{ {
text = instant.Value.ToUnixTimeMilliseconds().ToString(); text = instant.Value.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture);
} }
break; break;
@ -297,7 +300,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
if (instant.Success) if (instant.Success)
{ {
text = instant.Value.ToUnixTimeSeconds().ToString(); text = instant.Value.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);
} }
break; break;

8
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -364,7 +364,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return @event.GetType().Name; return @event.GetType().Name;
} }
public async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job) public async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job,
CancellationToken ct = default)
{ {
var actionWatch = ValueStopwatch.StartNew(); var actionWatch = ValueStopwatch.StartNew();
@ -379,7 +380,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules
using (var cts = new CancellationTokenSource(GetTimeoutInMs())) using (var cts = new CancellationTokenSource(GetTimeoutInMs()))
{ {
result = await actionHandler.ExecuteJobAsync(deserialized, cts.Token).WithCancellation(cts.Token); using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct))
{
result = await actionHandler.ExecuteJobAsync(deserialized, combined.Token).WithCancellation(combined.Token);
}
} }
} }
catch (Exception ex) catch (Exception ex)

3
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Globalization;
using Jint; using Jint;
using Jint.Native; using Jint.Native;
using Jint.Native.Object; using Jint.Native.Object;
@ -107,7 +108,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
for (var i = 0; i < arr.Length; i++) for (var i = 0; i < arr.Length; i++)
{ {
result.Add(Map(arr.Get(i.ToString()))); result.Add(Map(arr.Get(i.ToString(CultureInfo.InvariantCulture))));
} }
return result; return result;

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs

@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
private static async Task<JsValue> ParseResponse(ExecutionContext context, HttpResponseMessage response) private static async Task<JsValue> ParseResponse(ExecutionContext context, HttpResponseMessage response)
{ {
var responseString = await response.Content.ReadAsStringAsync(); var responseString = await response.Content.ReadAsStringAsync(context.CancellationToken);
context.CancellationToken.ThrowIfCancellationRequested(); context.CancellationToken.ThrowIfCancellationRequested();

6
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs

@ -14,9 +14,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
public interface IScriptEngine public interface IScriptEngine
{ {
Task<IJsonValue> ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default, CancellationToken ct = default); Task<IJsonValue> ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default,
CancellationToken ct = default);
Task<ContentData> TransformAsync(ScriptVars vars, string script, ScriptOptions options = default, CancellationToken ct = default); Task<ContentData> TransformAsync(ScriptVars vars, string script, ScriptOptions options = default,
CancellationToken ct = default);
IJsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default); IJsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default);

4
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
var tcs = new TaskCompletionSource<IJsonValue>(); var tcs = new TaskCompletionSource<IJsonValue>();
using (combined.Token.Register(() => tcs.TrySetCanceled())) await using (combined.Token.Register(() => tcs.TrySetCanceled(combined.Token)))
{ {
var context = var context =
CreateEngine(options) CreateEngine(options)
@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
var tcs = new TaskCompletionSource<ContentData>(); var tcs = new TaskCompletionSource<ContentData>();
using (combined.Token.Register(() => tcs.TrySetCanceled())) await using (combined.Token.Register(() => tcs.TrySetCanceled(combined.Token)))
{ {
var context = var context =
CreateEngine(options) CreateEngine(options)

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs

@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
private static JsValue CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable<Claim> allClaims) private static JsValue CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable<Claim> allClaims)
{ {
var claims = var claims =
allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last()) allClaims.GroupBy(x => x.Type.Split(ClaimSeparators)[^1])
.ToDictionary( .ToDictionary(
x => x.Key, x => x.Key,
x => x.Select(y => y.Value).ToArray()); x => x.Select(y => y.Value).ToArray());

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
public bool AsContext { get; set; } public bool AsContext { get; set; }
public override string ToString() public override readonly string ToString()
{ {
return $"CanReject={CanReject}, CanDisallow={CanDisallow}, AsContext={AsContext}"; return $"CanReject={CanReject}, CanDisallow={CanDisallow}, AsContext={AsContext}";
} }

4
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -18,6 +18,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" /> <PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" />
<PackageReference Include="GeoJSON.Net" Version="1.2.19" /> <PackageReference Include="GeoJSON.Net" Version="1.2.19" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.670">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.9.0" /> <PackageReference Include="Microsoft.OData.Core" Version="7.9.0" />
<PackageReference Include="NJsonSchema" Version="10.5.2" /> <PackageReference Include="NJsonSchema" Version="10.5.2" />

3
backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Globalization;
using Fluid; using Fluid;
using Fluid.Values; using Fluid.Values;
using NodaTime; using NodaTime;
@ -86,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions
private static FluidValue Format(FilterArguments arguments, DateTimeOffset value) private static FluidValue Format(FilterArguments arguments, DateTimeOffset value)
{ {
var formatted = value.ToString(arguments.At(0).ToStringValue()); var formatted = value.ToString(arguments.At(0).ToStringValue(), CultureInfo.InvariantCulture);
return new StringValue(formatted); return new StringValue(formatted);
} }

13
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -19,13 +19,20 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
private readonly Regex regex; private readonly Regex regex;
private readonly string? errorMessage; private readonly string? errorMessage;
public PatternValidator(string pattern, string? errorMessage = null) public PatternValidator(string pattern, string? errorMessage = null, bool capture = false)
{ {
Guard.NotNullOrEmpty(pattern, nameof(pattern)); Guard.NotNullOrEmpty(pattern, nameof(pattern));
this.errorMessage = errorMessage; this.errorMessage = errorMessage;
regex = new Regex($"^{pattern}$", RegexOptions.None, Timeout); var options = RegexOptions.None;
if (!capture)
{
options |= RegexOptions.ExplicitCapture;
}
regex = new Regex($"^{pattern}$", options, Timeout);
} }
public Task ValidateAsync(object? value, ValidationContext context, AddError addError) public Task ValidateAsync(object? value, ValidationContext context, AddError addError)

46
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs

@ -0,0 +1,46 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Entities.Apps.DomainObject;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
{
public sealed class MongoAppEntity : MongoState<AppDomainObject.State>
{
[BsonRequired]
[BsonElement("_an")]
public string IndexedName { get; set; }
[BsonRequired]
[BsonElement("_ui")]
public string[] IndexedUserIds { get; set; }
[BsonRequired]
[BsonElement("_dl")]
public bool IndexedDeleted { get; set; }
public override void Prepare()
{
var users = new HashSet<string>
{
Document.CreatedBy.Identifier
};
users.AddRange(Document.Contributors.Keys);
users.AddRange(Document.Clients.Keys);
IndexedUserIds = users.ToArray();
IndexedDeleted = Document.IsDeleted;
IndexedName = Document.Name;
}
}
}

86
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.DomainObject;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
{
public sealed class MongoAppRepository : MongoSnapshotStoreBase<AppDomainObject.State, MongoAppEntity>, IAppRepository, IDeleter
{
public MongoAppRepository(IMongoDatabase database, JsonSerializer jsonSerializer)
: base(database, jsonSerializer)
{
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoAppEntity> collection,
CancellationToken ct)
{
return collection.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<MongoAppEntity>(
Index
.Ascending(x => x.IndexedName)),
new CreateIndexModel<MongoAppEntity>(
Index
.Ascending(x => x.IndexedUserIds))
}, ct);
}
Task IDeleter.DeleteAppAsync(IAppEntity app,
CancellationToken ct)
{
return Collection.DeleteManyAsync(Filter.Eq(x => x.DocumentId, app.Id), ct);
}
public async Task<Dictionary<string, DomainId>> QueryIdsAsync(string contributorId,
CancellationToken ct = default)
{
using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryIdsAsync"))
{
var find = Collection.Find(x => x.IndexedUserIds.Contains(contributorId) && !x.IndexedDeleted);
return await QueryAsync(find, ct);
}
}
public async Task<Dictionary<string, DomainId>> QueryIdsAsync(IEnumerable<string> names,
CancellationToken ct = default)
{
using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAsync"))
{
var find = Collection.Find(x => names.Contains(x.IndexedName) && !x.IndexedDeleted);
return await QueryAsync(find, ct);
}
}
private static async Task<Dictionary<string, DomainId>> QueryAsync(IFindFluent<MongoAppEntity, MongoAppEntity> find,
CancellationToken ct)
{
var entities = await find.Only(x => x.DocumentId, x => x.IndexedName).ToListAsync(ct);
return entities.Select(x =>
{
var indexedId = DomainId.Create(x["_id"].AsString);
var indexedName = x["_an"].AsString;
return new { indexedName, indexedId };
}).ToDictionary(x => x.indexedName, x => x.indexedId);
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetFolderEntity> collection, protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetFolderEntity> collection,
CancellationToken ct = default) CancellationToken ct)
{ {
return collection.Indexes.CreateManyAsync(new[] return collection.Indexes.CreateManyAsync(new[]
{ {

43
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs

@ -5,13 +5,13 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Domain.Apps.Entities.Assets.DomainObject;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -20,15 +20,28 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
public sealed partial class MongoAssetFolderRepository : ISnapshotStore<AssetFolderDomainObject.State> public sealed partial class MongoAssetFolderRepository : ISnapshotStore<AssetFolderDomainObject.State>, IDeleter
{ {
async Task<(AssetFolderDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<AssetFolderDomainObject.State>.ReadAsync(DomainId key) Task IDeleter.DeleteAppAsync(IAppEntity app,
CancellationToken ct)
{
return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct);
}
IAsyncEnumerable<(AssetFolderDomainObject.State State, long Version)> ISnapshotStore<AssetFolderDomainObject.State>.ReadAllAsync(
CancellationToken ct)
{
return Collection.Find(new BsonDocument(), Batching.Options).ToAsyncEnumerable(ct).Select(x => (Map(x), x.Version));
}
async Task<(AssetFolderDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<AssetFolderDomainObject.State>.ReadAsync(DomainId key,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/ReadAsync")) using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/ReadAsync"))
{ {
var existing = var existing =
await Collection.Find(x => x.DocumentId == key) await Collection.Find(x => x.DocumentId == key)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync(ct);
if (existing != null) if (existing != null)
{ {
@ -39,17 +52,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
} }
async Task ISnapshotStore<AssetFolderDomainObject.State>.WriteAsync(DomainId key, AssetFolderDomainObject.State value, long oldVersion, long newVersion) async Task ISnapshotStore<AssetFolderDomainObject.State>.WriteAsync(DomainId key, AssetFolderDomainObject.State value, long oldVersion, long newVersion,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteAsync")) using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteAsync"))
{ {
var entity = Map(value); var entity = Map(value);
await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, entity); await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, entity, ct);
} }
} }
async Task ISnapshotStore<AssetFolderDomainObject.State>.WriteManyAsync(IEnumerable<(DomainId Key, AssetFolderDomainObject.State Value, long Version)> snapshots) async Task ISnapshotStore<AssetFolderDomainObject.State>.WriteManyAsync(IEnumerable<(DomainId Key, AssetFolderDomainObject.State Value, long Version)> snapshots,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteManyAsync")) using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteManyAsync"))
{ {
@ -66,24 +81,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
return; return;
} }
await Collection.BulkWriteAsync(updates, BulkUnordered); await Collection.BulkWriteAsync(updates, BulkUnordered, ct);
} }
} }
async Task ISnapshotStore<AssetFolderDomainObject.State>.ReadAllAsync(Func<AssetFolderDomainObject.State, long, Task> callback, async Task ISnapshotStore<AssetFolderDomainObject.State>.RemoveAsync(DomainId key,
CancellationToken ct) CancellationToken ct)
{
using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/ReadAllAsync"))
{
await Collection.Find(new BsonDocument(), Batching.Options).ForEachAsync(x => callback(Map(x), x.Version), ct);
}
}
async Task ISnapshotStore<AssetFolderDomainObject.State>.RemoveAsync(DomainId key)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/RemoveAsync")) using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/RemoveAsync"))
{ {
await Collection.DeleteOneAsync(x => x.DocumentId == key); await Collection.DeleteOneAsync(x => x.DocumentId == key, ct);
} }
} }

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -39,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetEntity> collection, protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetEntity> collection,
CancellationToken ct = default) CancellationToken ct)
{ {
return collection.Indexes.CreateManyAsync(new[] return collection.Indexes.CreateManyAsync(new[]
{ {
@ -142,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
return ResultList.Create<IAssetEntity>(assetTotal, assetEntities); return ResultList.Create<IAssetEntity>(assetTotal, assetEntities);
} }
} }
catch (MongoQueryException ex) when (ex.Message.Contains("17406")) catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal))
{ {
throw new DomainException(T.Get("common.resultTooLarge")); throw new DomainException(T.Get("common.resultTooLarge"));
} }

43
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs

@ -5,13 +5,13 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Domain.Apps.Entities.Assets.DomainObject;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -20,15 +20,28 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
public sealed partial class MongoAssetRepository : ISnapshotStore<AssetDomainObject.State> public sealed partial class MongoAssetRepository : ISnapshotStore<AssetDomainObject.State>, IDeleter
{ {
async Task<(AssetDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<AssetDomainObject.State>.ReadAsync(DomainId key) Task IDeleter.DeleteAppAsync(IAppEntity app,
CancellationToken ct)
{
return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct);
}
IAsyncEnumerable<(AssetDomainObject.State State, long Version)> ISnapshotStore<AssetDomainObject.State>.ReadAllAsync(
CancellationToken ct)
{
return Collection.Find(new BsonDocument(), Batching.Options).ToAsyncEnumerable(ct).Select(x => (Map(x), x.Version));
}
async Task<(AssetDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<AssetDomainObject.State>.ReadAsync(DomainId key,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetRepository/ReadAsync")) using (Telemetry.Activities.StartActivity("MongoAssetRepository/ReadAsync"))
{ {
var existing = var existing =
await Collection.Find(x => x.DocumentId == key) await Collection.Find(x => x.DocumentId == key)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync(ct);
if (existing != null) if (existing != null)
{ {
@ -39,17 +52,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
} }
async Task ISnapshotStore<AssetDomainObject.State>.WriteAsync(DomainId key, AssetDomainObject.State value, long oldVersion, long newVersion) async Task ISnapshotStore<AssetDomainObject.State>.WriteAsync(DomainId key, AssetDomainObject.State value, long oldVersion, long newVersion,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteAsync")) using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteAsync"))
{ {
var entity = Map(value); var entity = Map(value);
await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, entity); await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, entity, ct);
} }
} }
async Task ISnapshotStore<AssetDomainObject.State>.WriteManyAsync(IEnumerable<(DomainId Key, AssetDomainObject.State Value, long Version)> snapshots) async Task ISnapshotStore<AssetDomainObject.State>.WriteManyAsync(IEnumerable<(DomainId Key, AssetDomainObject.State Value, long Version)> snapshots,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteManyAsync")) using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteManyAsync"))
{ {
@ -66,24 +81,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
return; return;
} }
await Collection.BulkWriteAsync(updates, BulkUnordered); await Collection.BulkWriteAsync(updates, BulkUnordered, ct);
} }
} }
async Task ISnapshotStore<AssetDomainObject.State>.ReadAllAsync(Func<AssetDomainObject.State, long, Task> callback, async Task ISnapshotStore<AssetDomainObject.State>.RemoveAsync(DomainId key,
CancellationToken ct) CancellationToken ct)
{
using (Telemetry.Activities.StartActivity("MongoAssetRepository/ReadAllAsync"))
{
await Collection.Find(new BsonDocument(), Batching.Options).ForEachAsync(x => callback(Map(x), x.Version), ct);
}
}
async Task ISnapshotStore<AssetDomainObject.State>.RemoveAsync(DomainId key)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetRepository/RemoveAsync")) using (Telemetry.Activities.StartActivity("MongoAssetRepository/RemoveAsync"))
{ {
await Collection.DeleteOneAsync(x => x.DocumentId == key); await Collection.DeleteOneAsync(x => x.DocumentId == key, ct);
} }
} }

44
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -103,6 +103,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return queryAsStream.StreamAll(appId, schemaIds, ct); return queryAsStream.StreamAll(appId, schemaIds, ct);
} }
public IAsyncEnumerable<IContentEntity> QueryScheduledWithoutDataAsync(Instant now,
CancellationToken ct)
{
return queryScheduled.QueryAsync(now, ct);
}
public async Task DeleteAppAsync(DomainId appId,
CancellationToken ct)
{
using (Telemetry.Activities.StartActivity("MongoContentCollection/DeleteAppAsync"))
{
await Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, appId), ct);
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q, public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q,
CancellationToken ct) CancellationToken ct)
{ {
@ -165,15 +180,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
} }
public async Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback,
CancellationToken ct)
{
using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryScheduledWithoutDataAsync"))
{
await queryScheduled.QueryAsync(now, callback, ct);
}
}
public async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids, public async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, HashSet<DomainId> ids,
CancellationToken ct) CancellationToken ct)
{ {
@ -201,24 +207,28 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
} }
public async Task<long> FindVersionAsync(DomainId documentId) public async Task<long> FindVersionAsync(DomainId documentId,
CancellationToken ct = default)
{ {
var result = await Collection.Find(x => x.DocumentId == documentId).Only(x => x.Version).FirstOrDefaultAsync(); var result = await Collection.Find(x => x.DocumentId == documentId).Only(x => x.Version).FirstOrDefaultAsync(ct);
return result?["vs"].AsInt64 ?? EtagVersion.Empty; return result?["vs"].AsInt64 ?? EtagVersion.Empty;
} }
public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity) public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity,
CancellationToken ct = default)
{ {
return Collection.UpsertVersionedAsync(documentId, oldVersion, entity.Version, entity); return Collection.UpsertVersionedAsync(documentId, oldVersion, entity.Version, entity, ct);
} }
public Task RemoveAsync(DomainId documentId) public Task RemoveAsync(DomainId documentId,
CancellationToken ct = default)
{ {
return Collection.DeleteOneAsync(x => x.DocumentId == documentId); return Collection.DeleteOneAsync(x => x.DocumentId == documentId, ct);
} }
public Task InsertManyAsync(IReadOnlyList<MongoContentEntity> entities) public Task InsertManyAsync(IReadOnlyList<MongoContentEntity> entities,
CancellationToken ct = default)
{ {
if (entities.Count == 0) if (entities.Count == 0)
{ {
@ -232,7 +242,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
IsUpsert = true IsUpsert = true
}).ToList(); }).ToList();
return Collection.BulkWriteAsync(writes, BulkUnordered); return Collection.BulkWriteAsync(writes, BulkUnordered, ct);
} }
} }
} }

15
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -47,7 +47,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
this.appProvider = appProvider; this.appProvider = appProvider;
} }
public async Task InitializeAsync(CancellationToken ct = default) public async Task InitializeAsync(
CancellationToken ct)
{ {
await collectionAll.InitializeAsync(ct); await collectionAll.InitializeAsync(ct);
await collectionPublished.InitializeAsync(ct); await collectionPublished.InitializeAsync(ct);
@ -59,6 +60,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return collectionAll.StreamAll(appId, schemaIds, ct); return collectionAll.StreamAll(appId, schemaIds, ct);
} }
public IAsyncEnumerable<IContentEntity> QueryScheduledWithoutDataAsync(Instant now,
CancellationToken ct = default)
{
return collectionAll.QueryScheduledWithoutDataAsync(now, ct);
}
public Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q, SearchScope scope, public Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, List<ISchemaEntity> schemas, Q q, SearchScope scope,
CancellationToken ct = default) CancellationToken ct = default)
{ {
@ -130,12 +137,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return collectionAll.ResetScheduledAsync(documentId, ct); return collectionAll.ResetScheduledAsync(documentId, ct);
} }
public Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback,
CancellationToken ct = default)
{
return collectionAll.QueryScheduledWithoutDataAsync(now, callback, ct);
}
public Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode, public Task<IReadOnlyList<(DomainId SchemaId, DomainId Id, Status Status)>> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode,
CancellationToken ct = default) CancellationToken ct = default)
{ {

74
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -5,12 +5,13 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.DomainObject; using Squidex.Domain.Apps.Entities.Contents.DomainObject;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -18,43 +19,57 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
public partial class MongoContentRepository : ISnapshotStore<ContentDomainObject.State> public partial class MongoContentRepository : ISnapshotStore<ContentDomainObject.State>, IDeleter
{ {
Task ISnapshotStore<ContentDomainObject.State>.ReadAllAsync(Func<ContentDomainObject.State, long, Task> callback, IAsyncEnumerable<(ContentDomainObject.State State, long Version)> ISnapshotStore<ContentDomainObject.State>.ReadAllAsync(
CancellationToken ct) CancellationToken ct)
{ {
return Task.CompletedTask; return AsyncEnumerable.Empty<(ContentDomainObject.State State, long Version)>();
} }
async Task<(ContentDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<ContentDomainObject.State>.ReadAsync(DomainId key) async Task<(ContentDomainObject.State Value, bool Valid, long Version)> ISnapshotStore<ContentDomainObject.State>.ReadAsync(DomainId key,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoContentRepository/ReadAsync")) using (Telemetry.Activities.StartActivity("MongoContentRepository/ReadAsync"))
{ {
var version = await collectionAll.FindVersionAsync(key); var version = await collectionAll.FindVersionAsync(key, ct);
return (null!, false, version); return (null!, false, version);
} }
} }
async Task ISnapshotStore<ContentDomainObject.State>.ClearAsync() async Task IDeleter.DeleteAppAsync(IAppEntity app,
CancellationToken ct)
{
using (Telemetry.Activities.StartActivity("MongoContentRepository/DeleteAppAsync"))
{
await collectionAll.DeleteAppAsync(app.Id, ct);
await collectionPublished.DeleteAppAsync(app.Id, ct);
}
}
async Task ISnapshotStore<ContentDomainObject.State>.ClearAsync(
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoContentRepository/ClearAsync")) using (Telemetry.Activities.StartActivity("MongoContentRepository/ClearAsync"))
{ {
await collectionAll.ClearAsync(); await collectionAll.ClearAsync(ct);
await collectionPublished.ClearAsync(); await collectionPublished.ClearAsync(ct);
} }
} }
async Task ISnapshotStore<ContentDomainObject.State>.RemoveAsync(DomainId key) async Task ISnapshotStore<ContentDomainObject.State>.RemoveAsync(DomainId key,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoContentRepository/RemoveAsync")) using (Telemetry.Activities.StartActivity("MongoContentRepository/RemoveAsync"))
{ {
await collectionAll.RemoveAsync(key); await collectionAll.RemoveAsync(key, ct);
await collectionPublished.RemoveAsync(key); await collectionPublished.RemoveAsync(key, ct);
} }
} }
async Task ISnapshotStore<ContentDomainObject.State>.WriteAsync(DomainId key, ContentDomainObject.State value, long oldVersion, long newVersion) async Task ISnapshotStore<ContentDomainObject.State>.WriteAsync(DomainId key, ContentDomainObject.State value, long oldVersion, long newVersion,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteAsync")) using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteAsync"))
{ {
@ -64,12 +79,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
await Task.WhenAll( await Task.WhenAll(
UpsertDraftContentAsync(value, oldVersion, newVersion), UpsertDraftContentAsync(value, oldVersion, newVersion, ct),
UpsertOrDeletePublishedAsync(value, oldVersion, newVersion)); UpsertOrDeletePublishedAsync(value, oldVersion, newVersion, ct));
} }
} }
async Task ISnapshotStore<ContentDomainObject.State>.WriteManyAsync(IEnumerable<(DomainId Key, ContentDomainObject.State Value, long Version)> snapshots) async Task ISnapshotStore<ContentDomainObject.State>.WriteManyAsync(IEnumerable<(DomainId Key, ContentDomainObject.State Value, long Version)> snapshots,
CancellationToken ct)
{ {
using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteManyAsync")) using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteManyAsync"))
{ {
@ -87,42 +103,46 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
await Task.WhenAll( await Task.WhenAll(
collectionPublished.InsertManyAsync(entitiesPublished), collectionPublished.InsertManyAsync(entitiesPublished, ct),
collectionAll.InsertManyAsync(entitiesAll)); collectionAll.InsertManyAsync(entitiesAll, ct));
} }
} }
private async Task UpsertOrDeletePublishedAsync(ContentDomainObject.State value, long oldVersion, long newVersion) private async Task UpsertOrDeletePublishedAsync(ContentDomainObject.State value, long oldVersion, long newVersion,
CancellationToken ct = default)
{ {
if (ShouldWritePublished(value)) if (ShouldWritePublished(value))
{ {
await UpsertPublishedContentAsync(value, oldVersion, newVersion); await UpsertPublishedContentAsync(value, oldVersion, newVersion, ct);
} }
else else
{ {
await DeletePublishedContentAsync(value.AppId.Id, value.Id); await DeletePublishedContentAsync(value.AppId.Id, value.Id, ct);
} }
} }
private Task DeletePublishedContentAsync(DomainId appId, DomainId id) private Task DeletePublishedContentAsync(DomainId appId, DomainId id,
CancellationToken ct = default)
{ {
var documentId = DomainId.Combine(appId, id); var documentId = DomainId.Combine(appId, id);
return collectionPublished.RemoveAsync(documentId); return collectionPublished.RemoveAsync(documentId, ct);
} }
private async Task UpsertDraftContentAsync(ContentDomainObject.State value, long oldVersion, long newVersion) private async Task UpsertDraftContentAsync(ContentDomainObject.State value, long oldVersion, long newVersion,
CancellationToken ct = default)
{ {
var entity = await CreateDraftContentAsync(value, newVersion); var entity = await CreateDraftContentAsync(value, newVersion);
await collectionAll.UpsertVersionedAsync(entity.DocumentId, oldVersion, entity); await collectionAll.UpsertVersionedAsync(entity.DocumentId, oldVersion, entity, ct);
} }
private async Task UpsertPublishedContentAsync(ContentDomainObject.State value, long oldVersion, long newVersion) private async Task UpsertPublishedContentAsync(ContentDomainObject.State value, long oldVersion, long newVersion,
CancellationToken ct = default)
{ {
var entity = await CreatePublishedContentAsync(value, newVersion); var entity = await CreatePublishedContentAsync(value, newVersion);
await collectionPublished.UpsertVersionedAsync(entity.DocumentId, oldVersion, entity); await collectionPublished.UpsertVersionedAsync(entity.DocumentId, oldVersion, entity, ct);
} }
private async Task<MongoContentEntity> CreatePublishedContentAsync(ContentDomainObject.State value, long newVersion) private async Task<MongoContentEntity> CreatePublishedContentAsync(ContentDomainObject.State value, long newVersion)

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs

@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
} }
else else
{ {
var first = documentIds.First(); var first = documentIds[0];
filters.Add( filters.Add(
Filter.Or( Filter.Or(

9
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -64,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
try try
{ {
var schema = await appProvider.GetSchemaAsync(appId, schemaId); var schema = await appProvider.GetSchemaAsync(appId, schemaId, ct: ct);
if (schema == null) if (schema == null)
{ {
@ -81,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
throw new DomainException(T.Get("common.resultTooLarge")); throw new DomainException(T.Get("common.resultTooLarge"));
} }
catch (MongoQueryException ex) when (ex.Message.Contains("17406")) catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal))
{ {
throw new DomainException(T.Get("common.resultTooLarge")); throw new DomainException(T.Get("common.resultTooLarge"));
} }
@ -117,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
throw new DomainException(T.Get("common.resultTooLarge")); throw new DomainException(T.Get("common.resultTooLarge"));
} }
catch (MongoQueryException ex) when (ex.Message.Contains("17406")) catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal))
{ {
throw new DomainException(T.Get("common.resultTooLarge")); throw new DomainException(T.Get("common.resultTooLarge"));
} }
@ -154,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
throw new DomainException(T.Get("common.resultTooLarge")); throw new DomainException(T.Get("common.resultTooLarge"));
} }
catch (MongoQueryException ex) when (ex.Message.Contains("17406")) catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal))
{ {
throw new DomainException(T.Get("common.resultTooLarge")); throw new DomainException(T.Get("common.resultTooLarge"));
} }

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
internal class QueryReferences : OperationBase internal sealed class QueryReferences : OperationBase
{ {
private static readonly IResultList<IContentEntity> EmptyIds = ResultList.CreateFrom<IContentEntity>(0); private static readonly IResultList<IContentEntity> EmptyIds = ResultList.CreateFrom<IContentEntity>(0);
private readonly QueryByIds queryByIds; private readonly QueryByIds queryByIds;

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

Loading…
Cancel
Save